diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fe2414bdf30..7e89f85d2cc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,6 +14,9 @@ updates: patterns: - "org.dhatim:fastexcel" - "org.dhatim:fastexcel-reader" + bouncycastle: + patterns: + - "org.bouncycastle:*" - package-ecosystem: npm directory: "/ui" diff --git a/.gradle-wrapper/gradle-wrapper.properties b/.gradle-wrapper/gradle-wrapper.properties index 0aaefbcaf0f..df97d72b8b9 100644 --- a/.gradle-wrapper/gradle-wrapper.properties +++ b/.gradle-wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/build.gradle b/build.gradle index 82249fb14f2..ad224209fd9 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ subprojects { } checkstyle { - toolVersion = '10.11.0' + toolVersion = '10.18.2' configFile = file("${rootDir}/cnf/checkstyle.xml") maxWarnings = 0 ignoreFailures = false diff --git a/cnf/build.bnd b/cnf/build.bnd index 00cba399ba6..dbc9605619d 100644 --- a/cnf/build.bnd +++ b/cnf/build.bnd @@ -41,6 +41,7 @@ buildpath: \ org.osgi.service.metatype.annotations;version='1.4.1',\ org.osgi.util.promise;version='1.2.0',\ com.google.guava;version='33.3.1.jre',\ + com.google.guava.failureaccess;version='1.0.2',\ com.google.gson;version='2.11.0',\ testpath: \ diff --git a/cnf/checkstyle.xml b/cnf/checkstyle.xml index ad22318becf..6ed4ad0febb 100644 --- a/cnf/checkstyle.xml +++ b/cnf/checkstyle.xml @@ -23,8 +23,8 @@ - + @@ -47,9 +47,9 @@ + - @@ -107,8 +107,8 @@ - + @@ -144,9 +144,9 @@ + - @@ -179,12 +179,10 @@ - - - + - + @@ -206,15 +204,15 @@ - + - + diff --git a/cnf/pom.xml b/cnf/pom.xml index 83b8c69ddd9..4445a228051 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -139,6 +139,34 @@ 1.5 + + + + io.helins + linux-common + 0.1.4 + + + + + io.helins + linux-i2c + 1.0.2 + + + + + io.helins + linux-io + 0.0.4 + + + + + io.helins + linux-errno + 1.0.2 + io.reactivex.rxjava3 rxjava @@ -253,9 +281,17 @@ + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + + + + org.bouncycastle - bcpkix-jdk15on - 1.70 + bcprov-jdk18on + 1.78.1 org.dhatim @@ -301,7 +337,7 @@ org.jetbrains.kotlin kotlin-osgi-bundle - 2.0.20 + 2.0.21 org.jetbrains.kotlinx @@ -414,4 +450,4 @@ 3.9 - + \ No newline at end of file diff --git a/doc/modules/ROOT/assets/images/deploy-docker-backend-check-version.png b/doc/modules/ROOT/assets/images/deploy-docker-backend-check-version.png new file mode 100644 index 00000000000..351a863a197 Binary files /dev/null and b/doc/modules/ROOT/assets/images/deploy-docker-backend-check-version.png differ diff --git a/doc/modules/ROOT/assets/images/deploy-docker-backend.png b/doc/modules/ROOT/assets/images/deploy-docker-backend.png new file mode 100644 index 00000000000..64a1ee887a1 Binary files /dev/null and b/doc/modules/ROOT/assets/images/deploy-docker-backend.png differ diff --git a/doc/modules/ROOT/assets/images/deploy-docker-edge-check-version.png b/doc/modules/ROOT/assets/images/deploy-docker-edge-check-version.png new file mode 100644 index 00000000000..c2bddc8d89e Binary files /dev/null and b/doc/modules/ROOT/assets/images/deploy-docker-edge-check-version.png differ diff --git a/doc/modules/ROOT/assets/images/deploy-docker-edge.png b/doc/modules/ROOT/assets/images/deploy-docker-edge.png new file mode 100644 index 00000000000..22d46700d49 Binary files /dev/null and b/doc/modules/ROOT/assets/images/deploy-docker-edge.png differ diff --git a/doc/modules/ROOT/assets/images/deploy-docker-ssh.png b/doc/modules/ROOT/assets/images/deploy-docker-ssh.png new file mode 100644 index 00000000000..291a103d2d2 Binary files /dev/null and b/doc/modules/ROOT/assets/images/deploy-docker-ssh.png differ diff --git a/doc/modules/ROOT/pages/backend/deploy.adoc b/doc/modules/ROOT/pages/backend/deploy.adoc index b7c19ea5c83..0c6b873909a 100644 --- a/doc/modules/ROOT/pages/backend/deploy.adoc +++ b/doc/modules/ROOT/pages/backend/deploy.adoc @@ -8,25 +8,27 @@ :icons: font :imagesdir: ../../assets/images +== Debian Linux + This chapter explains how OpenEMS Backend can be deployed on a Debian Linux server. Similar techniques will work for other operating systems as well. -== Prepare operating system environment +=== Prepare operating system environment NOTE: It is recommended to run every service on a server with limited permissions. This example runs OpenEMS Backend with user "root" which is a bad idea for a production server! -=== Create an application directory +==== Create an application directory Create the directory */opt/openems-backend*. This is going to be the place, where we put the JAR file. Execute `mkdir /opt/openems-backend`. -=== Create a config directory +==== Create a config directory Create the directory */opt/openems-backend/config.d*. This is going to be the place, where all the bundle configurations are held. Execute `mkdir /opt/openems-backend/config.d`. -=== Create a systemd service definition +==== Create a systemd service definition The systemd 'Service Manager' manages system processes in a Debian Linux. We will create a systemd service definition file, so that systemd takes care of managing (starting/restarting/...) the OpenEMS Backend service. @@ -78,3 +80,82 @@ To update the OpenEMS JAR file at the target device, it is required to copy the Execute `systemctl restart openems-backend --no-block; journalctl -lfu openems-backend` + The command restarts the service (_systemctl restart openems-backend_) while not waiting for the OpenEMS startup notification (_--no-block_). Then it directly prints the OpenEMS system log (_journalctl -lfu openems-backend_). + +== Docker + +This chapter explains how OpenEMS Backend can be deployed using our official https://hub.docker.com/r/openems/backend[Docker image]. + +=== Prepare system + +==== Connect to the server + +image::deploy-docker-ssh.png[SSH into device] + +==== Check docker installation + +image::deploy-docker-backend-check-version.png[Check docker installation] + +__if not already installed, follow <>__ + +==== Setup docker + +To setup docker follow the instuctions from https://docs.docker.com/engine/install/[docs.docker.com]. + +=== Create a Docker compose + +Paste content into a `docker-compose.yml` +---- +services: + openems_backend: + image: openems/backend:latest + container_name: openems_backend + hostname: openems_backend + restart: unless-stopped + volumes: + - openems-backend-conf:/var/opt/openems/config:rw + - openems-backend-data:/var/opt/openems/data:rw + ports: + - 8079:8079 # Apache-Felix + - 8081:8081 # Edge-Websocket + - 8082:8082 # UI-Websocket + + openems-ui: + image: openems/ui-backend:latest + container_name: openems_ui + hostname: openems_ui + restart: unless-stopped + volumes: + - openems-ui-conf:/etc/nginx:rw + - openems-ui-log:/var/log/nginx:rw + environment: + - UI_WEBSOCKET=ws://:8082 # Change to your actual hostname or ip + ports: + - 80:80 + - 443:443 + +volumes: + openems-backend-conf: + openems-backend-data: + openems-ui-conf: + openems-ui-log: +---- + +=== Run compose file + +To start the previously created `docker-compose.yml` run the command: +---- +docker compose up -d +---- + +=== Check logs + +To check if the container is up and running, check `docker ps`: + +image::deploy-docker-backend.png[docker ps] + +or read its logs with: +---- +docker logs openems_backend +---- + +NOTE: If you want to run the backand with an InfluxDB instance as well, see: https://github.com/OpenEMS/openems/tree/develop/tools/docker/backend. \ No newline at end of file diff --git a/doc/modules/ROOT/pages/edge/deploy.adoc b/doc/modules/ROOT/pages/edge/deploy.adoc index 58059b150a3..631099c92c2 100644 --- a/doc/modules/ROOT/pages/edge/deploy.adoc +++ b/doc/modules/ROOT/pages/edge/deploy.adoc @@ -8,6 +8,8 @@ :icons: font :imagesdir: ../../assets/images +== Debian Linux + This chapter explains how OpenEMS can be deployed on a Debian Linux Internet-of-Things Gateway. Similar techniques will work for other operating systems as well. This guide covers a simple, manual approach. For productive systems it is required to automate deployment to IoT devices. Good approaches include a Debian package repository that provides *.deb-files and third-party tools like http://www.eclipse.org/hawkbit/[Eclipse Hawkbit]. This is out-of-scope for this small guide. @@ -19,7 +21,7 @@ Prerequisites: * Setup an SSH client to connect to the Linux console, e.g. http://www.9bis.net/kitty/[KiTTY] * Setup an SCP client to copy the JAR file via SSH, e.g. https://winscp.net/eng/docs/lang:de[WinSCP] -== Connect via SSH and SCP +=== Connect via SSH and SCP . Connect via SSH using KiTTY .. Open KiTTY and connect to the target device. @@ -132,3 +134,74 @@ The command restarts the service (_systemctl restart openems_) while not waiting + .OpenEMS Edge start-up image::deploy-openems-start.png[OpenEMS Edge start-up] + + +== Docker + +This chapter explains how OpenEMS can be deployed using our official https://hub.docker.com/r/openems/edge[Docker image]. + +Prerequisites: + +* A amd64 or arm64 device running Linux. You need the IP address and SSH access. +* A working docker environment. To setup follow instruction from https://docs.docker.com/engine/install/[docs.docker.com]. + +=== Prepare system + +==== Connect to the device + +image::deploy-docker-ssh.png[SSH into device] + +==== Check docker installation + +image::deploy-docker-edge-check-version.png[Check docker installation] + +__if not already installed, follow <>__ + +==== Setup docker + +To setup docker follow the instuctions from https://docs.docker.com/engine/install/[docs.docker.com]. + +=== Start Container + +==== Create a Docker compose + +Paste content into a `docker-compose.yml` +---- +services: + openems-edge: + image: openems/edge:latest + container_name: openems_edge + hostname: openems_edge + restart: unless-stopped + volumes: + - openems-edge-conf:/var/opt/openems/config:rw + - openems-edge-data:/var/opt/openems/data:rw + ports: + - 8080:8080 # Apache-Felix + +volumes: + openems-edge-conf: + openems-edge-data: +---- + +==== Run compose file + +To start the previously created `docker-compose.yml` run the command: +---- +docker compose up -d +---- + +==== Check logs + +To check if the container is up and running, check `docker ps`: + +image::deploy-docker-edge.png[docker ps] + +or read its logs with: +---- +docker logs openems_edge +---- + +--- + +NOTE: If you want to start a UI instance as well, see: https://github.com/OpenEMS/openems/tree/develop/tools/docker/edge. \ No newline at end of file diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index e210938f4fd..e02d41046c8 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -115,7 +115,7 @@ org.apache.felix.scr;version='[2.2.12,2.2.13)',\ org.apache.felix.webconsole;version='[5.0.8,5.0.9)',\ org.apache.felix.webconsole.plugins.ds;version='[2.3.0,2.3.1)',\ - org.jetbrains.kotlin.osgi-bundle;version='[2.0.20,2.0.21)',\ + org.jetbrains.kotlin.osgi-bundle;version='[2.0.21,2.0.22)',\ org.jsr-305;version='[3.0.2,3.0.3)',\ org.ops4j.pax.logging.pax-logging-api;version='[2.2.1,2.2.2)',\ org.ops4j.pax.logging.pax-logging-log4j2;version='[2.2.1,2.2.2)',\ diff --git a/io.openems.backend.application/test/io/openems/backend/edge/application/TestClient.java b/io.openems.backend.application/test/io/openems/backend/edge/application/TestClient.java index c6030384134..12c4a97ac8a 100644 --- a/io.openems.backend.application/test/io/openems/backend/edge/application/TestClient.java +++ b/io.openems.backend.application/test/io/openems/backend/edge/application/TestClient.java @@ -92,12 +92,7 @@ public void setOnNotification(OnNotification onNotification) { @Override protected WsData createWsData(WebSocket ws) { - return new WsData(ws) { - @Override - public String toString() { - return "TestClient.WsData []"; - } - }; + return new WsData(ws); } @Override diff --git a/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/WsData.java b/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/WsData.java index 24298d8c0f2..9276a41a2c1 100644 --- a/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/WsData.java +++ b/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/WsData.java @@ -65,7 +65,11 @@ public SubscribedEdgesChannelsWorker getSubscribedChannelsWorker() { } @Override - public String toString() { - return "B2bWebsocket.WsData [user=" + this.user.getNow(null) + "]"; + public String toLogString() { + var user = this.user.getNow(null); + var userId = user == null // + ? "UNDEFINED" // + : user.getId(); + return "B2bWebsocket.WsData [user=" + userId + "]"; } } diff --git a/io.openems.backend.b2bwebsocket/test/io/openems/backend/b2bwebsocket/TestClient.java b/io.openems.backend.b2bwebsocket/test/io/openems/backend/b2bwebsocket/TestClient.java index 3f313d2c93b..1e04b149df7 100644 --- a/io.openems.backend.b2bwebsocket/test/io/openems/backend/b2bwebsocket/TestClient.java +++ b/io.openems.backend.b2bwebsocket/test/io/openems/backend/b2bwebsocket/TestClient.java @@ -93,12 +93,7 @@ public void setOnNotification(OnNotification onNotification) { @Override protected WsData createWsData(WebSocket ws) { - return new WsData(ws) { - @Override - public String toString() { - return "TestClient.WsData []"; - } - }; + return new WsData(ws); } @Override diff --git a/io.openems.backend.b2bwebsocket/test/io/openems/backend/b2bwebsocket/WsDataTest.java b/io.openems.backend.b2bwebsocket/test/io/openems/backend/b2bwebsocket/WsDataTest.java new file mode 100644 index 00000000000..eeedb52b567 --- /dev/null +++ b/io.openems.backend.b2bwebsocket/test/io/openems/backend/b2bwebsocket/WsDataTest.java @@ -0,0 +1,35 @@ +package io.openems.backend.b2bwebsocket; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.util.Optional; + +import org.junit.Test; + +import io.openems.backend.common.metadata.User; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; + +public class WsDataTest { + + @Test + public void test() throws OpenemsNamedException { + var sut = new WsData(null, null); + assertEquals("B2bWebsocket.WsData [user=UNDEFINED]", sut.toLogString()); + assertEquals(Optional.empty(), sut.getUserOpt()); + assertThrows(OpenemsNamedException.class, () -> sut.getUserWithTimeout(1, MILLISECONDS)); + assertEquals(null, sut.getUser().getNow(null)); + + var user = new User("foo", null, null, null, null, false, null); + sut.setUser(user); + assertEquals("B2bWebsocket.WsData [user=foo]", sut.toLogString()); + assertEquals(Optional.of(user), sut.getUserOpt()); + assertEquals(user, sut.getUserWithTimeout(1, MILLISECONDS)); + assertNotNull(sut.getSubscribedChannelsWorker()); + + sut.dispose(); + } + +} diff --git a/io.openems.backend.common/src/io/openems/backend/common/jsonrpc/SimulationEngine.java b/io.openems.backend.common/src/io/openems/backend/common/jsonrpc/SimulationEngine.java index 975f1d00ea6..16447638f6e 100644 --- a/io.openems.backend.common/src/io/openems/backend/common/jsonrpc/SimulationEngine.java +++ b/io.openems.backend.common/src/io/openems/backend/common/jsonrpc/SimulationEngine.java @@ -5,6 +5,7 @@ import io.openems.backend.common.jsonrpc.request.SimulationRequest; import io.openems.backend.common.metadata.User; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; public interface SimulationEngine { diff --git a/io.openems.backend.core/src/io/openems/backend/core/jsonrpcrequesthandler/EdgeRpcRequestHandler.java b/io.openems.backend.core/src/io/openems/backend/core/jsonrpcrequesthandler/EdgeRpcRequestHandler.java index c98e855c17d..5c4e6ef0109 100644 --- a/io.openems.backend.core/src/io/openems/backend/core/jsonrpcrequesthandler/EdgeRpcRequestHandler.java +++ b/io.openems.backend.core/src/io/openems/backend/core/jsonrpcrequesthandler/EdgeRpcRequestHandler.java @@ -5,8 +5,12 @@ import static io.openems.common.utils.JsonUtils.getAsString; import static java.util.Collections.emptyMap; +import java.io.IOException; +import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -15,6 +19,7 @@ import io.openems.backend.common.metadata.User; import io.openems.common.exceptions.OpenemsError; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.common.jsonrpc.request.AppCenterRequest; import io.openems.common.jsonrpc.request.ComponentJsonApiRequest; @@ -29,7 +34,13 @@ import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesDataResponse; import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesEnergyPerPeriodResponse; import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesEnergyResponse; +import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesExportXlsxResponse; +import io.openems.common.session.Language; import io.openems.common.session.Role; +import io.openems.common.timedata.Resolution; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportDataEntry.HistoricTimedataSaveType; +import io.openems.common.timedata.XlsxExportUtil; +import io.openems.common.types.ChannelAddress; public class EdgeRpcRequestHandler { @@ -265,8 +276,38 @@ private CompletableFuture handleQueryHistoricEnergyPerPe */ private CompletableFuture handleQueryHistoricTimeseriesExportXlxsRequest(String edgeId, User user, QueryHistoricTimeseriesExportXlxsRequest request) throws OpenemsNamedException { - return CompletableFuture.completedFuture(this.parent.timedataManager - .handleQueryHistoricTimeseriesExportXlxsRequest(edgeId, request, user.getLanguage())); + return CompletableFuture.completedFuture( + this.handleQueryHistoricTimeseriesExportXlxsRequest(edgeId, request, user.getLanguage())); + } + + private QueryHistoricTimeseriesExportXlsxResponse handleQueryHistoricTimeseriesExportXlxsRequest(String edgeId, + QueryHistoricTimeseriesExportXlxsRequest request, Language language) throws OpenemsNamedException { + final var powerChannels = new TreeSet(QueryHistoricTimeseriesExportXlsxResponse.POWER_CHANNELS); + final var energyChannels = new TreeSet( + QueryHistoricTimeseriesExportXlsxResponse.ENERGY_CHANNELS); + + final var edge = this.parent.metadata.edge().getEdgeConfig(edgeId); + + final var detailData = XlsxExportUtil.getDetailData(edge); + final var channelsByType = detailData.getChannelsBySaveType(); + powerChannels.addAll(channelsByType.getOrDefault(HistoricTimedataSaveType.POWER, Collections.emptyList())); + energyChannels.addAll(channelsByType.getOrDefault(HistoricTimedataSaveType.ENERGY, Collections.emptyList())); + + var powerData = this.parent.timedataManager.queryHistoricData(edgeId, request.getFromDate(), + request.getToDate(), powerChannels, new Resolution(15, ChronoUnit.MINUTES)); + + var energyData = this.parent.timedataManager.queryHistoricEnergy(edgeId, request.getFromDate(), + request.getToDate(), energyChannels); + if (powerData == null || energyData == null) { + return null; + } + try { + return new QueryHistoricTimeseriesExportXlsxResponse(request.getId(), edgeId, request.getFromDate(), + request.getToDate(), powerData, energyData, language, detailData); + + } catch (IOException e) { + throw new OpenemsException("QueryHistoricTimeseriesExportXlxsRequest failed: " + e.getMessage()); + } } /** diff --git a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/WsData.java b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/WsData.java index 180eaca7c5a..6ff6bab290d 100644 --- a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/WsData.java +++ b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/WsData.java @@ -76,10 +76,11 @@ public synchronized Optional getEdgeId() { } @Override - public String toString() { - return "EdgeWebsocket.WsData [" // - + "edgeId=" + this.edgeId.orElse("UNKNOWN") // - + "]"; + protected String toLogString() { + return new StringBuilder("EdgeWebsocket.WsData [edgeId=") // + .append(this.edgeId.orElse("UNKNOWN")) // + .append("]") // + .toString(); } } diff --git a/io.openems.backend.edgewebsocket/test/io/openems/backend/edgewebsocket/WsDataTest.java b/io.openems.backend.edgewebsocket/test/io/openems/backend/edgewebsocket/WsDataTest.java new file mode 100644 index 00000000000..707168f40e3 --- /dev/null +++ b/io.openems.backend.edgewebsocket/test/io/openems/backend/edgewebsocket/WsDataTest.java @@ -0,0 +1,37 @@ +package io.openems.backend.edgewebsocket; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.util.Optional; + +import org.junit.Test; + +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.jsonrpc.base.GenericJsonrpcNotification; +import io.openems.common.jsonrpc.base.JsonrpcMessage; + +public class WsDataTest { + + private static final String EDGE_ID = "edge0"; + private static final JsonrpcMessage JMSG = new GenericJsonrpcNotification("foo", new JsonObject()); + + @Test + public void test() throws OpenemsException { + var sut = new WsData(null); + assertEquals("EdgeWebsocket.WsData [edgeId=UNKNOWN]", sut.toLogString()); + assertThrows(OpenemsNamedException.class, () -> sut.assertEdgeId(JMSG)); + assertThrows(OpenemsNamedException.class, () -> sut.assertEdgeIdWithTimeout(JMSG, 1, MILLISECONDS)); + + sut.setEdgeId(EDGE_ID); + assertEquals("EdgeWebsocket.WsData [edgeId=edge0]", sut.toLogString()); + sut.assertEdgeId(null); + sut.assertEdgeIdWithTimeout(JMSG, 1, MILLISECONDS); + assertEquals(Optional.of(EDGE_ID), sut.getEdgeId()); + } + +} diff --git a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WsData.java b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WsData.java index c60424b0d3d..b2bfdca34ef 100644 --- a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WsData.java +++ b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WsData.java @@ -154,14 +154,15 @@ public String assertToken() throws OpenemsNamedException { } @Override - public String toString() { - String tokenString; - if (this.token.isPresent()) { - tokenString = this.token.get().toString(); - } else { - tokenString = "UNKNOWN"; - } - return "UiWebsocket.WsData [userId=" + this.userId.orElse("UNKNOWN") + ", token=" + tokenString + "]"; + protected String toLogString() { + return new StringBuilder("UiWebsocket.WsData [userId=") // + .append(this.userId.orElse("UNKNOWN")) // + .append(", token=") // + .append(this.token.isPresent() // + ? this.token.get().toString() // + : "UNKNOWN") // + .append("]") // + .toString(); } /** diff --git a/io.openems.backend.uiwebsocket/test/io/openems/backend/uiwebsocket/impl/WsDataTest.java b/io.openems.backend.uiwebsocket/test/io/openems/backend/uiwebsocket/impl/WsDataTest.java new file mode 100644 index 00000000000..8832a3c6dfd --- /dev/null +++ b/io.openems.backend.uiwebsocket/test/io/openems/backend/uiwebsocket/impl/WsDataTest.java @@ -0,0 +1,35 @@ +package io.openems.backend.uiwebsocket.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.util.Optional; + +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; + +public class WsDataTest { + + private static final String USER_ID = "user0"; + private static final String TOKEN = "token"; + + @Test + public void test() throws OpenemsNamedException { + var sut = new WsData(null); + assertEquals(Optional.empty(), sut.getUser(null)); + assertThrows(OpenemsNamedException.class, () -> sut.assertToken()); + assertEquals("UiWebsocket.WsData [userId=UNKNOWN, token=UNKNOWN]", sut.toLogString()); + + sut.setUserId(USER_ID); + sut.setToken(TOKEN); + + assertEquals(Optional.of(USER_ID), sut.getUserId()); + assertEquals(Optional.of(TOKEN), sut.getToken()); + assertEquals(TOKEN, sut.assertToken()); + assertEquals("UiWebsocket.WsData [userId=user0, token=token]", sut.toLogString()); + + sut.logout(); + } + +} diff --git a/io.openems.common/resources/templates/controller/$testSrcDir$/$basePackageDir$/MyControllerTest.java b/io.openems.common/resources/templates/controller/$testSrcDir$/$basePackageDir$/MyControllerTest.java index 2205624e13f..41b5f380e43 100644 --- a/io.openems.common/resources/templates/controller/$testSrcDir$/$basePackageDir$/MyControllerTest.java +++ b/io.openems.common/resources/templates/controller/$testSrcDir$/$basePackageDir$/MyControllerTest.java @@ -7,15 +7,14 @@ public class MyControllerTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { new ControllerTest(new MyControllerImpl()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .build()) - .next(new TestCase()); + .setId("ctrl0") // + .build()) // + .next(new TestCase()) // + .deactivate(); } } diff --git a/io.openems.common/resources/templates/device-modbus/$testSrcDir$/$basePackageDir$/MyModbusDeviceTest.java b/io.openems.common/resources/templates/device-modbus/$testSrcDir$/$basePackageDir$/MyModbusDeviceTest.java index c9961ad3a77..eb60ad3df76 100644 --- a/io.openems.common/resources/templates/device-modbus/$testSrcDir$/$basePackageDir$/MyModbusDeviceTest.java +++ b/io.openems.common/resources/templates/device-modbus/$testSrcDir$/$basePackageDir$/MyModbusDeviceTest.java @@ -9,19 +9,17 @@ public class MyModbusDeviceTest { - private static final String COMPONENT_ID = "component0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MyModbusDeviceImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setModbusId(MODBUS_ID) // - .build()) - .next(new TestCase()); + .setId("component0") // + .setModbusId("modbus0") // + .build()) // + .next(new TestCase()) // + .deactivate(); } } diff --git a/io.openems.common/resources/templates/device/$testSrcDir$/$basePackageDir$/MyDeviceTest.java b/io.openems.common/resources/templates/device/$testSrcDir$/$basePackageDir$/MyDeviceTest.java index d5cb74df7c9..f02eed1d7f9 100644 --- a/io.openems.common/resources/templates/device/$testSrcDir$/$basePackageDir$/MyDeviceTest.java +++ b/io.openems.common/resources/templates/device/$testSrcDir$/$basePackageDir$/MyDeviceTest.java @@ -7,15 +7,14 @@ public class MyDeviceTest { - private static final String COMPONENT_ID = "component0"; - @Test public void test() throws Exception { new ComponentTest(new MyDeviceImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .build()) - .next(new TestCase()); + .setId("component0") // + .build()) // + .next(new TestCase()) // + .deactivate(); } } diff --git a/io.openems.common/src/io/openems/common/OpenemsConstants.java b/io.openems.common/src/io/openems/common/OpenemsConstants.java index 9b7384aae62..4ff2e3f1fab 100644 --- a/io.openems.common/src/io/openems/common/OpenemsConstants.java +++ b/io.openems.common/src/io/openems/common/OpenemsConstants.java @@ -22,7 +22,7 @@ public class OpenemsConstants { *

* This is the month of the release. */ - public static final short VERSION_MINOR = 10; + public static final short VERSION_MINOR = 11; /** * The patch version of OpenEMS. diff --git a/io.openems.common/src/io/openems/common/channel/Unit.java b/io.openems.common/src/io/openems/common/channel/Unit.java index 466ea4fbef6..2bcc50113fd 100644 --- a/io.openems.common/src/io/openems/common/channel/Unit.java +++ b/io.openems.common/src/io/openems/common/channel/Unit.java @@ -284,7 +284,12 @@ public enum Unit { /** * Unit of Pressure [bar]. */ - BAR("bar"); + BAR("bar"), + + /** + * Unit of Pressure [mbar]. + */ + MILLIBAR("mbar", BAR, -3); public final String symbol; public final Unit baseUnit; @@ -363,7 +368,7 @@ public String format(Object value, OpenemsType type) { MILLIWATT, WATT_HOURS, OHM, KILOOHM, SECONDS, AMPERE_HOURS, HOUR, CUMULATED_SECONDS, KILOAMPERE_HOURS, KILOVOLT_AMPERE, KILOVOLT_AMPERE_REACTIVE, KILOVOLT_AMPERE_REACTIVE_HOURS, KILOWATT_HOURS, MICROOHM, MILLIAMPERE_HOURS, MILLIOHM, MILLISECONDS, MINUTE, THOUSANDTH, VOLT_AMPERE_HOURS, - VOLT_AMPERE_REACTIVE_HOURS, WATT_HOURS_BY_WATT_PEAK, CUMULATED_WATT_HOURS, BAR -> // + VOLT_AMPERE_REACTIVE_HOURS, WATT_HOURS_BY_WATT_PEAK, CUMULATED_WATT_HOURS, BAR, MILLIBAR -> // value + " " + this.symbol; case ON_OFF -> // diff --git a/io.openems.common/src/io/openems/common/jsonrpc/response/QueryHistoricTimeseriesExportXlsxResponse.java b/io.openems.common/src/io/openems/common/jsonrpc/response/QueryHistoricTimeseriesExportXlsxResponse.java index 6158477f132..acf64aba840 100644 --- a/io.openems.common/src/io/openems/common/jsonrpc/response/QueryHistoricTimeseriesExportXlsxResponse.java +++ b/io.openems.common/src/io/openems/common/jsonrpc/response/QueryHistoricTimeseriesExportXlsxResponse.java @@ -6,15 +6,19 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map.Entry; import java.util.ResourceBundle; import java.util.Set; import java.util.SortedMap; import java.util.UUID; +import java.util.function.BiFunction; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.dhatim.fastexcel.BorderSide; +import org.dhatim.fastexcel.BorderStyle; import org.dhatim.fastexcel.Workbook; import org.dhatim.fastexcel.Worksheet; @@ -22,6 +26,10 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.session.Language; +import io.openems.common.timedata.XlsxExportDetailData; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportCategory; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportDataEntry; +import io.openems.common.timedata.XlsxWorksheetWrapper; import io.openems.common.types.ChannelAddress; import io.openems.common.utils.JsonUtils; @@ -60,6 +68,11 @@ protected static class Channel { public static final ChannelAddress ESS_SOC = new ChannelAddress("_sum", "EssSoc"); } + private static final String BLUE = "44B3E1"; + private static final String LIGHT_GREY = "BFBFBF"; + private static final String DARK_GREY = "D9D9D9"; + private static final String FONT_NAME = "Calibri"; + /** * All Power Channels, i.e. Channels that are exported per channel and * timestamp. @@ -91,21 +104,24 @@ protected static class Channel { * While constructing, the actual Excel file is generated as payload of the * JSON-RPC Response. * - * @param id the JSON-RPC ID - * @param edgeId the Edge-ID - * @param fromDate the start date of the export - * @param toDate the end date of the export - * @param historicData the power data per channel and timestamp - * @param historicEnergy the energy data, one value per channel - * @param language the {@link Language} + * @param id the JSON-RPC ID + * @param edgeId the Edge-ID + * @param fromDate the start date of the export + * @param toDate the end date of the export + * @param historicData the power data per channel and timestamp + * @param historicEnergy the energy data, one value per channel + * @param language the {@link Language} + * @param detailComponents the components for the detail view * @throws IOException on error * @throws OpenemsNamedException on error */ public QueryHistoricTimeseriesExportXlsxResponse(UUID id, String edgeId, ZonedDateTime fromDate, ZonedDateTime toDate, SortedMap> historicData, - SortedMap historicEnergy, Language language) - throws IOException, OpenemsNamedException { - super(id, XlsxUtils.generatePayload(edgeId, fromDate, toDate, historicData, historicEnergy, language)); + SortedMap historicEnergy, Language language, + XlsxExportDetailData detailComponents) throws IOException, OpenemsNamedException { + + super(id, XlsxUtils.generatePayload(edgeId, fromDate, toDate, historicData, historicEnergy, language, + detailComponents)); } protected static class XlsxUtils { @@ -118,20 +134,21 @@ protected static class XlsxUtils { * Generates the Payload for a * {@link QueryHistoricTimeseriesExportXlsxResponse}. * - * @param edgeId the Edge-Id - * @param fromDate the start date of the export - * @param toDate the end date of the export - * @param powerData the power data per channel and timestamp - * @param energyData the energy data, one value per channel - * @param language the {@link Language} + * @param edgeId the Edge-Id + * @param fromDate the start date of the export + * @param toDate the end date of the export + * @param powerData the power data per channel and timestamp + * @param energyData the energy data, one value per channel + * @param language the {@link Language} + * @param detailComponents the components for the detail view * @return the Excel file as byte-array. * @throws IOException on error * @throws OpenemsNamedException on error */ private static byte[] generatePayload(String edgeId, ZonedDateTime fromDate, ZonedDateTime toDate, SortedMap> powerData, - SortedMap energyData, Language language) - throws IOException, OpenemsNamedException { + SortedMap energyData, Language language, + XlsxExportDetailData detailComponents) throws IOException, OpenemsNamedException { byte[] payload = {}; try (// var os = new ByteArrayOutputStream(); @@ -146,8 +163,54 @@ private static byte[] generatePayload(String edgeId, ZonedDateTime fromDate, Zon XlsxUtils.addBasicInfo(ws, edgeId, fromDate, toDate, translationBundle); XlsxUtils.addEnergyData(ws, energyData, translationBundle); - XlsxUtils.addPowerData(ws, powerData, translationBundle); + var rowCount = XlsxUtils.addPowerData(ws, powerData, translationBundle); + + final var worksheetWrapper = new XlsxWorksheetWrapper(ws); + + // Box for Total Overview + worksheetWrapper.setForRange(9, 0, 9, 7, t -> t.style().borderStyle(BorderSide.TOP, BorderStyle.THIN)); + worksheetWrapper.setForRange(9, 0, rowCount, 0, + t -> t.style().borderStyle(BorderSide.LEFT, BorderStyle.THIN)); + worksheetWrapper.setForRange(rowCount, 0, rowCount, 7, + t -> t.style().borderStyle(BorderSide.BOTTOM, BorderStyle.THIN)); + worksheetWrapper.setForRange(9, 7, rowCount, 7, + t -> t.style().borderStyle(BorderSide.RIGHT, BorderStyle.THIN)); + + if (detailComponents.data().values().stream().anyMatch(de -> !de.isEmpty())) { // + var rightestColumns = XlsxUtils.addDetailData(ws, powerData, detailComponents, translationBundle, + energyData); + + final var colProd = rightestColumns.get(0) - 1; + final var colCons = rightestColumns.get(1) - 1; + final var colTou = rightestColumns.get(2) - 1; + + // Set Box for detail Timerange data + worksheetWrapper.setForRange(9, 10, 9, colTou, + t -> t.style().borderStyle(BorderSide.TOP, BorderStyle.THIN)); + worksheetWrapper.setForRange(9, 10, rowCount, 10, + t -> t.style().borderStyle(BorderSide.LEFT, BorderStyle.THIN)); + worksheetWrapper.setForRange(rowCount, 10, rowCount, colTou, + t -> t.style().borderStyle(BorderSide.BOTTOM, BorderStyle.THIN)); + worksheetWrapper.setForRange(9, colTou, rowCount, colTou, + t -> t.style().borderStyle(BorderSide.RIGHT, BorderStyle.THIN)); + + // Set Separators between prod, cons and tou + worksheetWrapper.setForRange(9, colProd, rowCount, colProd, + t -> t.style().borderStyle(BorderSide.RIGHT, BorderStyle.THIN)); + worksheetWrapper.setForRange(9, colCons, rowCount, colCons, + t -> t.style().borderStyle(BorderSide.RIGHT, BorderStyle.THIN)); + + // Set "blue" Separator between detailed Overview and total Overview + worksheetWrapper.setForRange(0, 8, rowCount, 8, t -> t.style()// + .borderStyle(BorderSide.LEFT, BorderStyle.MEDIUM)// + .borderStyle(BorderSide.RIGHT, BorderStyle.MEDIUM)// + .fillColor(BLUE)); + worksheetWrapper.getCellWrapper(0, 8).style().borderStyle(BorderSide.TOP, BorderStyle.MEDIUM); + worksheetWrapper.getCellWrapper(rowCount, 8).style().borderStyle(BorderSide.BOTTOM, + BorderStyle.MEDIUM); + } + worksheetWrapper.setAll(); wb.finish(); os.flush(); payload = os.toByteArray(); @@ -199,29 +262,35 @@ protected static void addBasicInfo(Worksheet ws, String edgeId, ZonedDateTime fr */ protected static void addEnergyData(Worksheet ws, SortedMap data, ResourceBundle translationBundle) throws OpenemsNamedException { + ws.range(4, 1, 4, 6).merge(); + ws.value(4, 1, translationBundle.getString("totalOverview")); + ws.range(4, 1, 4, 6).style().fontName(FONT_NAME).fontSize(12).horizontalAlignment("center") + .verticalAlignment("center").bold().fillColor(BLUE).set(); + ws.range(5, 1, 5, 6).style().bold().fillColor(LIGHT_GREY).set(); + ws.range(6, 1, 6, 6).style().fillColor(DARK_GREY).set(); // Grid buy energy - XlsxUtils.addStringValueBold(ws, 4, 1, translationBundle.getString("gridBuy") + " [kWh]"); - XlsxUtils.addKwhValueIfnotNull(ws, 5, 1, data.get(Channel.GRID_BUY_ACTIVE_ENERGY), translationBundle); + XlsxUtils.addStringValueBold(ws, 5, 1, translationBundle.getString("gridBuy") + " [kWh]"); + XlsxUtils.addKwhValueIfnotNull(ws, 6, 1, data.get(Channel.GRID_BUY_ACTIVE_ENERGY), translationBundle); // Grid sell energy - XlsxUtils.addStringValueBold(ws, 4, 2, translationBundle.getString("gridFeedIn") + " [kWh]"); - XlsxUtils.addKwhValueIfnotNull(ws, 5, 2, data.get(Channel.GRID_SELL_ACTIVE_ENERGY), translationBundle); + XlsxUtils.addStringValueBold(ws, 5, 2, translationBundle.getString("gridFeedIn") + " [kWh]"); + XlsxUtils.addKwhValueIfnotNull(ws, 6, 2, data.get(Channel.GRID_SELL_ACTIVE_ENERGY), translationBundle); // Production energy - XlsxUtils.addStringValueBold(ws, 4, 3, translationBundle.getString("production") + " [kWh]"); - XlsxUtils.addKwhValueIfnotNull(ws, 5, 3, data.get(Channel.PRODUCTION_ACTIVE_ENERGY), translationBundle); + XlsxUtils.addStringValueBold(ws, 5, 3, translationBundle.getString("production") + " [kWh]"); + XlsxUtils.addKwhValueIfnotNull(ws, 6, 3, data.get(Channel.PRODUCTION_ACTIVE_ENERGY), translationBundle); // Charge energy - XlsxUtils.addStringValueBold(ws, 4, 4, translationBundle.getString("storageCharging") + " [kWh]"); - XlsxUtils.addKwhValueIfnotNull(ws, 5, 4, data.get(Channel.ESS_DC_CHARGE_ENERGY), translationBundle); + XlsxUtils.addStringValueBold(ws, 5, 4, translationBundle.getString("storageCharging") + " [kWh]"); + XlsxUtils.addKwhValueIfnotNull(ws, 6, 4, data.get(Channel.ESS_DC_CHARGE_ENERGY), translationBundle); // Charge energy - XlsxUtils.addStringValueBold(ws, 4, 5, translationBundle.getString("storageDischarging") + " [kWh]"); - XlsxUtils.addKwhValueIfnotNull(ws, 5, 5, data.get(Channel.ESS_DC_DISCHARGE_ENERGY), translationBundle); + XlsxUtils.addStringValueBold(ws, 5, 5, translationBundle.getString("storageDischarging") + " [kWh]"); + XlsxUtils.addKwhValueIfnotNull(ws, 6, 5, data.get(Channel.ESS_DC_DISCHARGE_ENERGY), translationBundle); // Consumption energy - XlsxUtils.addStringValueBold(ws, 4, 6, translationBundle.getString("consumption") + " [kWh]"); - XlsxUtils.addKwhValueIfnotNull(ws, 5, 6, data.get(Channel.CONSUMPTION_ACTIVE_ENERGY), translationBundle); + XlsxUtils.addStringValueBold(ws, 5, 6, translationBundle.getString("consumption") + " [kWh]"); + XlsxUtils.addKwhValueIfnotNull(ws, 6, 6, data.get(Channel.CONSUMPTION_ACTIVE_ENERGY), translationBundle); } /** @@ -230,22 +299,24 @@ protected static void addEnergyData(Worksheet ws, SortedMap> data, ResourceBundle translationBundle) throws OpenemsNamedException { // Adding the headers - XlsxUtils.addStringValueBold(ws, 7, 0, translationBundle.getString("date/time")); - XlsxUtils.addStringValueBold(ws, 7, 1, translationBundle.getString("gridBuy") + " [W]"); - XlsxUtils.addStringValueBold(ws, 7, 2, translationBundle.getString("gridFeedIn") + " [W]"); - XlsxUtils.addStringValueBold(ws, 7, 3, translationBundle.getString("production") + " [W]"); - XlsxUtils.addStringValueBold(ws, 7, 4, translationBundle.getString("storageCharging") + " [W]"); - XlsxUtils.addStringValueBold(ws, 7, 5, translationBundle.getString("storageDischarging") + " [W]"); - XlsxUtils.addStringValueBold(ws, 7, 6, translationBundle.getString("consumption") + " [W]"); - XlsxUtils.addStringValueBold(ws, 7, 7, translationBundle.getString("stateOfCharge") + " [%]"); - - var rowCount = 8; + XlsxUtils.addStringValueBold(ws, 9, 0, translationBundle.getString("date/time")); + XlsxUtils.addStringValueBold(ws, 9, 1, translationBundle.getString("gridBuy") + " [W]"); + XlsxUtils.addStringValueBold(ws, 9, 2, translationBundle.getString("gridFeedIn") + " [W]"); + XlsxUtils.addStringValueBold(ws, 9, 3, translationBundle.getString("production") + " [W]"); + XlsxUtils.addStringValueBold(ws, 9, 4, translationBundle.getString("storageCharging") + " [W]"); + XlsxUtils.addStringValueBold(ws, 9, 5, translationBundle.getString("storageDischarging") + " [W]"); + XlsxUtils.addStringValueBold(ws, 9, 6, translationBundle.getString("consumption") + " [W]"); + XlsxUtils.addStringValueBold(ws, 9, 7, translationBundle.getString("stateOfCharge") + " [%]"); + XlsxUtils.addStringValueBold(ws, 8, 1, translationBundle.getString("generalData")); + + var rowCount = 10; for (Entry> row : data.entrySet()) { var values = row.getValue(); @@ -267,12 +338,17 @@ protected static void addPowerData(Worksheet ws, // Grid sell power XlsxUtils.addFloatValue(ws, rowCount, 2, gridActivePower / -1); } + } else { + XlsxUtils.addStringValue(ws, rowCount, 1, "-"); + XlsxUtils.addStringValue(ws, rowCount, 2, "-"); } // Production power if (XlsxUtils.isNotNull(values.get(Channel.PRODUCTION_ACTIVE_POWER))) { XlsxUtils.addFloatValue(ws, rowCount, 3, JsonUtils.getAsFloat(values.get(Channel.PRODUCTION_ACTIVE_POWER))); + } else { + XlsxUtils.addStringValue(ws, rowCount, 3, "-"); } if (XlsxUtils.isNotNull(values.get(Channel.ESS_DISCHARGE_POWER))) { @@ -284,19 +360,120 @@ protected static void addPowerData(Worksheet ws, XlsxUtils.addFloatValue(ws, rowCount, 4, essDischargePower / -1); XlsxUtils.addFloatValue(ws, rowCount, 5, 0); } + } else { + XlsxUtils.addStringValue(ws, rowCount, 4, "-"); + XlsxUtils.addStringValue(ws, rowCount, 5, "-"); } // Consumption power if (XlsxUtils.isNotNull(values.get(Channel.CONSUMPTION_ACTIVE_POWER))) { XlsxUtils.addFloatValue(ws, rowCount, 6, JsonUtils.getAsFloat(values.get(Channel.CONSUMPTION_ACTIVE_POWER))); + } else { + XlsxUtils.addStringValue(ws, rowCount, 6, "-"); } // State of charge if (XlsxUtils.isNotNull(values.get(Channel.ESS_SOC))) { XlsxUtils.addFloatValue(ws, rowCount, 7, JsonUtils.getAsFloat(values.get(Channel.ESS_SOC))); + } else { + XlsxUtils.addStringValue(ws, rowCount, 7, "-"); } rowCount++; } + rowCount--; + return rowCount; + } + + protected static List addDetailData(Worksheet ws, + SortedMap> data, + XlsxExportDetailData detailComponents, ResourceBundle translationBundle, + SortedMap energyData) throws OpenemsNamedException { + ws.width(8, 4); + ws.width(9, 4); + ws.width(10, 25); + ws.width(11, 25); + ws.width(12, 25); + ws.width(13, 25); + ws.width(14, 25); + ws.width(15, 25); + + ws.range(4, 10, 4, 15).merge(); + ws.range(4, 10, 4, 15).style().fontName(FONT_NAME).fontSize(12).bold().fillColor(BLUE).set(); + ws.value(4, 10, translationBundle.getString("detailData")); + ws.range(5, 10, 5, 15).merge(); + ws.range(5, 10, 5, 15).style().fillColor(LIGHT_GREY).set(); + ws.value(5, 10, translationBundle.getString("detailHint")); + var rightestColumn1 = addProductionData(ws, data, detailComponents, translationBundle); + var rightestColumn2 = addConsumptionData(ws, data, detailComponents, rightestColumn1, translationBundle); + var rightestColumn3 = addTimeOfUseTariffData(ws, data, detailComponents, rightestColumn2, + translationBundle); + ws.width(rightestColumn3, 35); + return List.of(rightestColumn1, rightestColumn2, rightestColumn3); + } + + protected static int addProductionData(Worksheet ws, + SortedMap> data, + XlsxExportDetailData detailComponents, ResourceBundle translationBundle) throws OpenemsNamedException { + return XlsxUtils.addGenericData(ws, data, detailComponents, 10, translationBundle, + XlsxExportCategory.PRODUCTION, "production", (t, d) -> t.alias() + " [W]", 1); + } + + protected static int addConsumptionData(Worksheet ws, + SortedMap> data, + XlsxExportDetailData detailComponents, int righestColumn, ResourceBundle translationBundle) + throws OpenemsNamedException { + return XlsxUtils.addGenericData(ws, data, detailComponents, righestColumn, translationBundle, + XlsxExportCategory.CONSUMPTION, "consumption", (t, d) -> t.alias() + " [W]", 1); + } + + protected static int addTimeOfUseTariffData(Worksheet ws, + SortedMap> data, + XlsxExportDetailData detailComponents, int righestColumn, ResourceBundle translationBundle) + throws OpenemsNamedException { + final var unit = "[" + detailComponents.currency().getUnderPart() + "/kWh]"; + return XlsxUtils.addGenericData(ws, data, detailComponents, righestColumn, translationBundle, + XlsxExportCategory.TIME_OF_USE_TARIFF, "timeOfUse", (t, d) -> t.alias() + " " + unit, + 1000f / detailComponents.currency().getRatio()); + } + + protected static int addGenericData(// + Worksheet ws, // + SortedMap> data, // + XlsxExportDetailData detailComponents, // + int righestColumn, // + ResourceBundle translationBundle, // + XlsxExportDetailData.XlsxExportCategory category, // + String translationKey, // + BiFunction aliasBuilder, // + float ratio) // + throws OpenemsNamedException { + + final var righestColumnOld = righestColumn; + + for (var item : detailComponents.data().get(category)) { + ws.value(8, righestColumnOld, translationBundle.getString(translationKey)); + ws.style(8, righestColumnOld).bold().set(); + + ws.value(9, righestColumn, aliasBuilder.apply(item, detailComponents)); + ws.style(9, righestColumn).bold().set(); + + var rowCount = 10; + for (final var row : data.entrySet()) { + final var values = row.getValue(); + final var channelAddress = item.channel(); + + if (XlsxUtils.isNotNull(values.get(channelAddress))) { + XlsxUtils.addFloatValueNotRounded(ws, rowCount, righestColumn, + JsonUtils.getAsFloat(values.get(channelAddress)) / ratio); + } else { + XlsxUtils.addStringValue(ws, rowCount, righestColumn, "-"); + } + rowCount++; + } + righestColumn++; + } + + return righestColumn; } /** @@ -385,6 +562,19 @@ protected static void addFloatValue(Worksheet ws, int row, int col, float value) ws.value(row, col, Math.round(value)); } + /** + * Helper method to add the value to the excel sheet. The float value is + * mathematically rounded. + * + * @param ws the {@link Worksheet} + * @param row row number + * @param col column number + * @param value actual value in the sheet + */ + protected static void addFloatValueNotRounded(Worksheet ws, int row, int col, float value) { + ws.value(row, col, value); + } + /** * Simple helper method to check for null values. * diff --git a/io.openems.common/src/io/openems/common/jsonrpc/response/translation_de.properties b/io.openems.common/src/io/openems/common/jsonrpc/response/translation_de.properties index 1ac1c7f1860..4fdcc640291 100644 --- a/io.openems.common/src/io/openems/common/jsonrpc/response/translation_de.properties +++ b/io.openems.common/src/io/openems/common/jsonrpc/response/translation_de.properties @@ -8,4 +8,9 @@ storageDischarging = Speicher Entladung consumption = Verbrauch stateOfCharge = Ladezustand date/time = Datum / Uhrzeit -notAvailable = nicht vorhanden \ No newline at end of file +notAvailable = nicht vorhanden +detailData = Detaillierte Auswertung der Erzeuger, Verbraucher und Apps +detailHint = * Bitte beachten Sie, dass diese Werte bereits in den Leistungsdaten in Ihrer Gesamtübersicht enthalten sind. +timeOfUse = Dynamischer Stromtarif +totalOverview = Gesamtübersicht +generalData = Allgemeine Daten \ No newline at end of file diff --git a/io.openems.common/src/io/openems/common/jsonrpc/response/translation_en.properties b/io.openems.common/src/io/openems/common/jsonrpc/response/translation_en.properties index 4da846d76c2..40f8fc4d09a 100644 --- a/io.openems.common/src/io/openems/common/jsonrpc/response/translation_en.properties +++ b/io.openems.common/src/io/openems/common/jsonrpc/response/translation_en.properties @@ -8,4 +8,9 @@ storageDischarging = Storage Discharging consumption = Consumption stateOfCharge = State of Charge date/time = Date / Time -notAvailable = not available \ No newline at end of file +notAvailable = not available +detailData = Detailed evaluation of the producer, consumer and apps +detailHint = * Please note that these values are already included in the performance data in your Total Overview. +timeOfUse = Time of Use Tariff +totalOverview = Total Overview +generalData = General Data \ No newline at end of file diff --git a/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java b/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java index cd0a08830ca..7bca125287b 100644 --- a/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java +++ b/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java @@ -71,6 +71,7 @@ public SystemUpdateParams getSystemUpdateParams() { .put("App.TimeOfUseTariff.Hassfurt", "") // .put("App.TimeOfUseTariff.RabotCharge", "") // .put("App.TimeOfUseTariff.Stromdao", "") // + .put("App.TimeOfUseTariff.Swisspower", "") // .put("App.TimeOfUseTariff.Tibber", "") // .put("App.Api.ModbusTcp.ReadOnly", "") // .put("App.Api.ModbusTcp.ReadWrite", "") // diff --git a/io.openems.common/src/io/openems/common/oem/OpenemsEdgeOem.java b/io.openems.common/src/io/openems/common/oem/OpenemsEdgeOem.java index 5f768f896f3..84dd61fb86f 100644 --- a/io.openems.common/src/io/openems/common/oem/OpenemsEdgeOem.java +++ b/io.openems.common/src/io/openems/common/oem/OpenemsEdgeOem.java @@ -133,13 +133,4 @@ public default String getEntsoeToken() { return null; } - /** - * Gets the OEM Access-Key for Exchangerate.host (used by - * TimeOfUseTariff.ENTSO-E). - * - * @return the value - */ - public default String getExchangeRateAccesskey() { - return null; - } } diff --git a/io.openems.common/src/io/openems/common/test/AbstractComponentConfig.java b/io.openems.common/src/io/openems/common/test/AbstractComponentConfig.java index af56ff73a01..c8f81ad2338 100644 --- a/io.openems.common/src/io/openems/common/test/AbstractComponentConfig.java +++ b/io.openems.common/src/io/openems/common/test/AbstractComponentConfig.java @@ -1,5 +1,7 @@ package io.openems.common.test; +import static io.openems.common.utils.ReflectionUtils.invokeMethodViaReflection; + import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -110,7 +112,7 @@ public Dictionary getAsProperties() } var key = method.getName().replace("_", "."); - var value = method.invoke(this); + var value = invokeMethodViaReflection(this, method); if (value == null) { throw new IllegalArgumentException("Configuration for [" + key + "] is null"); } diff --git a/io.openems.common/src/io/openems/common/timedata/CommonTimedataService.java b/io.openems.common/src/io/openems/common/timedata/CommonTimedataService.java index fdebd1f183c..097f7c5aad9 100644 --- a/io.openems.common/src/io/openems/common/timedata/CommonTimedataService.java +++ b/io.openems.common/src/io/openems/common/timedata/CommonTimedataService.java @@ -1,6 +1,5 @@ package io.openems.common.timedata; -import java.io.IOException; import java.time.Period; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; @@ -10,46 +9,11 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; import io.openems.common.jsonrpc.request.QueryHistoricTimeseriesDataRequest; -import io.openems.common.jsonrpc.request.QueryHistoricTimeseriesExportXlxsRequest; -import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesExportXlsxResponse; -import io.openems.common.session.Language; import io.openems.common.types.ChannelAddress; public interface CommonTimedataService { - /** - * Handles a {@link QueryHistoricTimeseriesExportXlxsRequest}. Exports historic - * data to an Excel file. - * - * @param edgeId the Edge-ID - * @param request the {@link QueryHistoricTimeseriesExportXlxsRequest} request - * @param language the {@link Language} - * @return the {@link QueryHistoricTimeseriesExportXlsxResponse} - * @throws OpenemsNamedException on error - */ - public default QueryHistoricTimeseriesExportXlsxResponse handleQueryHistoricTimeseriesExportXlxsRequest( - String edgeId, QueryHistoricTimeseriesExportXlxsRequest request, Language language) - throws OpenemsNamedException { - var powerData = this.queryHistoricData(edgeId, request.getFromDate(), request.getToDate(), - QueryHistoricTimeseriesExportXlsxResponse.POWER_CHANNELS, new Resolution(15, ChronoUnit.MINUTES)); - - var energyData = this.queryHistoricEnergy(edgeId, request.getFromDate(), request.getToDate(), - QueryHistoricTimeseriesExportXlsxResponse.ENERGY_CHANNELS); - - if (powerData == null || energyData == null) { - return null; - } - - try { - return new QueryHistoricTimeseriesExportXlsxResponse(request.getId(), edgeId, request.getFromDate(), - request.getToDate(), powerData, energyData, language); - } catch (IOException e) { - throw new OpenemsException("QueryHistoricTimeseriesExportXlxsRequest failed: " + e.getMessage()); - } - } - /** * Calculates the time {@link Resolution} for the period. * diff --git a/io.openems.common/src/io/openems/common/timedata/Timeout.java b/io.openems.common/src/io/openems/common/timedata/Timeout.java new file mode 100644 index 00000000000..bb489594998 --- /dev/null +++ b/io.openems.common/src/io/openems/common/timedata/Timeout.java @@ -0,0 +1,55 @@ +package io.openems.common.timedata; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; + +public class Timeout { + + private Instant entryTime = Instant.MIN; + private Duration timeout; + + private Timeout(Duration duration) { + this.timeout = duration; + } + + /** + * Get the {@link Timeout} of seconds. + * + * @param timeout the amount seconds + * @return the {@link Timeout} + */ + public static Timeout ofSeconds(int timeout) { + return new Timeout(Duration.ofSeconds(timeout)); + } + + /** + * Get the {@link Timeout} of minutes. + * + * @param timeout the amount minutes + * @return the {@link Timeout} + */ + public static Timeout ofMinutes(int timeout) { + return new Timeout(Duration.ofMinutes(timeout)); + } + + /** + * Sets the entry time. + * + * @param clock the {@link Clock} + */ + public void start(Clock clock) { + this.entryTime = Instant.now(clock); + } + + /** + * Checks the whether time elapsed. + * + * @param clock the {@link Clock} + * @return true if time is elapsed + */ + public boolean elapsed(Clock clock) { + return Instant.now(clock).isAfter(this.entryTime.plus(this.timeout)); + } + +} diff --git a/io.openems.common/src/io/openems/common/timedata/XlsxExportDetailData.java b/io.openems.common/src/io/openems/common/timedata/XlsxExportDetailData.java new file mode 100644 index 00000000000..199bc345846 --- /dev/null +++ b/io.openems.common/src/io/openems/common/timedata/XlsxExportDetailData.java @@ -0,0 +1,37 @@ +package io.openems.common.timedata; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportDataEntry.HistoricTimedataSaveType; +import io.openems.common.types.ChannelAddress; +import io.openems.common.types.CurrencyConfig; + +public record XlsxExportDetailData(// + EnumMap> data, // + CurrencyConfig currency +) { + + public Map> getChannelsBySaveType() { + return this.data().values().stream().flatMap(List::stream).collect(Collectors.groupingBy( + XlsxExportDataEntry::type, Collectors.mapping(XlsxExportDataEntry::channel, Collectors.toList()))); + } + + public enum XlsxExportCategory { + CONSUMPTION, PRODUCTION, TIME_OF_USE_TARIFF + } + + public record XlsxExportDataEntry(// + String alias, ChannelAddress channel, // + HistoricTimedataSaveType type // + ) { + + public enum HistoricTimedataSaveType { + POWER, ENERGY + } + + } + +} diff --git a/io.openems.common/src/io/openems/common/timedata/XlsxExportUtil.java b/io.openems.common/src/io/openems/common/timedata/XlsxExportUtil.java new file mode 100644 index 00000000000..0e3a815a464 --- /dev/null +++ b/io.openems.common/src/io/openems/common/timedata/XlsxExportUtil.java @@ -0,0 +1,119 @@ +package io.openems.common.timedata; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Set; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportCategory; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportDataEntry; +import io.openems.common.types.ChannelAddress; +import io.openems.common.types.CurrencyConfig; +import io.openems.common.types.EdgeConfig; +import io.openems.common.types.MeterType; +import io.openems.common.utils.JsonUtils; + +public class XlsxExportUtil { + + /** + * Gathers the detail data for excel export. + * + * @param edge the edge + * @return the currency represented as a CurrencyConfig + * @throws OpenemsNamedException if component isnt found + */ + private static CurrencyConfig getCurrency(EdgeConfig edge) throws OpenemsNamedException { + return edge.getComponent("_meta") // + .flatMap(t -> t.getProperty("currency")) // + .flatMap(t -> JsonUtils.getAsOptionalEnum(CurrencyConfig.class, t)) // + .orElse(CurrencyConfig.EUR); + } + + /** + * Gathers the detail data for excel export. + * + * @param edgeConfig the {@link EdgeConfig} + * @return the {@link XlsxExportDetailData} + * @throws OpenemsNamedException if component is not found + */ + public static XlsxExportDetailData getDetailData(EdgeConfig edgeConfig) throws OpenemsNamedException { + final var enumMap = new EnumMap>(XlsxExportCategory.class); + final var consumption = new ArrayList(); + final var production = new ArrayList(); + final var tou = new ArrayList(); + + enumMap.put(XlsxExportCategory.PRODUCTION, production); + enumMap.put(XlsxExportCategory.CONSUMPTION, consumption); + enumMap.put(XlsxExportCategory.TIME_OF_USE_TARIFF, tou); + + for (var component : edgeConfig.getComponents().values()) { + final var factory = edgeConfig.getFactories().get(component.getFactoryId()); + if (factory == null) { + continue; + } + for (var nature : factory.getNatureIds()) { + // Electricity meter + switch (nature) { + case Natures.METER -> { + final var props = component.getProperties(); + var meterType = JsonUtils.getAsOptionalEnum(MeterType.class, props.get("type")) + .orElse(null); + if (meterType != null) { + var list = switch (meterType) { + case CONSUMPTION_METERED, CONSUMPTION_NOT_METERED, MANAGED_CONSUMPTION_METERED -> consumption; + case PRODUCTION -> production; + case GRID, PRODUCTION_AND_CONSUMPTION -> null; + }; + if (list != null) { + list.add(new XlsxExportDataEntry(component.getAlias(), + new ChannelAddress(component.getId(), "ActivePower"), + XlsxExportDataEntry.HistoricTimedataSaveType.POWER)); + } + continue; + } + + final var activePowerType = getActivePowerType(component.getFactoryId()); + if (activePowerType == null) { + continue; + } + enumMap.get(activePowerType) + .add(new XlsxExportDataEntry(component.getAlias(), + new ChannelAddress(component.getId(), "ActivePower"), + XlsxExportDataEntry.HistoricTimedataSaveType.POWER)); + } + case Natures.TIME_OF_USE_TARIFF -> { + tou.add(new XlsxExportDataEntry(component.getAlias(), new ChannelAddress("_sum", "GridBuyPrice"), + XlsxExportDataEntry.HistoricTimedataSaveType.POWER)); + } + } + } + } + return new XlsxExportDetailData(enumMap, XlsxExportUtil.getCurrency(edgeConfig)); + } + + private static XlsxExportCategory getActivePowerType(String factoryId) { + if (Natures.PRODUCTION_NATURES.contains(factoryId)) { + return XlsxExportCategory.PRODUCTION; + } else if (Natures.CONSUMPTION_NATURES.contains(factoryId)) { + return XlsxExportCategory.CONSUMPTION; + } + return null; + } + + private static final class Natures { + public static final String METER = "io.openems.edge.meter.api.ElectricityMeter"; + public static final String TIME_OF_USE_TARIFF = "io.openems.edge.timeofusetariff.api.TimeOfUseTariff"; + public static final Set PRODUCTION_NATURES = Set.of("Simulator.PvInverter", "Fenecon.Dess.PvMeter", + "Fenecon.Mini.PvMeter", "Kaco.BlueplanetHybrid10.PvInverter", "PvInverter.Cluster", + "PV-Inverter.Fronius", "PV-Inverter.KACO.blueplanet", "PV-Inverter.SMA.SunnyTripower", + "PV-Inverter.Kostal", "PV-Inverter.Solarlog", "Simulator.ProductionMeter.Acting", + "SolarEdge.PV-Inverter"); + + public static final Set CONSUMPTION_NATURES = Set.of("GoodWe.EmergencyPowerMeter", + "Simulator.NRCMeter.Acting", "Evcs.AlpitronicHypercharger", "Evcs.Dezony", "Evcs.Goe.ChargerHome", + "Evcs.HardyBarth", "Evcs.Keba.KeContact", "Evcs.Ocpp.Abl", "Evcs.Ocpp.IesKeywattSingle", + "Evcs.Spelsberg.SMART", "Evcs.Webasto.Next", "Evcs.Webasto.Unite"); + } + +} diff --git a/io.openems.common/src/io/openems/common/timedata/XlsxWorksheetWrapper.java b/io.openems.common/src/io/openems/common/timedata/XlsxWorksheetWrapper.java new file mode 100644 index 00000000000..faec99a684a --- /dev/null +++ b/io.openems.common/src/io/openems/common/timedata/XlsxWorksheetWrapper.java @@ -0,0 +1,46 @@ +package io.openems.common.timedata; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import org.dhatim.fastexcel.StyleSetter; +import org.dhatim.fastexcel.Worksheet; + +public class XlsxWorksheetWrapper { + public record XlsxCellWrapper(int c, int r, StyleSetter style) { + } + + private final Map> cellMap = new HashMap<>(); + private final Worksheet ws; + + public XlsxWorksheetWrapper(Worksheet ws) { + this.ws = ws; + } + + /** + * Gets a CellWrapper if exists; otherwise creates a new one and returns that. + * + * @param r row of the cell + * @param c column of the cell + * @return the XlsxCellWrapper + */ + public XlsxCellWrapper getCellWrapper(int r, int c) { + final var columns = this.cellMap.computeIfAbsent(r, row -> new HashMap<>()); + return columns.computeIfAbsent(c, col -> new XlsxCellWrapper(r, c, this.ws.style(r, c))); + } + + public void setAll() { + this.cellMap.values().stream().flatMap(map -> map.values().stream()).forEach(val -> val.style().set()); + } + + public void setForRange(int r1, int c1, int r2, int c2, Consumer styleSetterFunc) { + for (int row = r1; row <= r2; row++) { + for (int col = c1; col <= c2; col++) { + var cell = this.getCellWrapper(row, col); + styleSetterFunc.accept(cell);; + } + } + } + +} \ No newline at end of file diff --git a/io.openems.common/src/io/openems/common/types/CurrencyConfig.java b/io.openems.common/src/io/openems/common/types/CurrencyConfig.java new file mode 100644 index 00000000000..8c401aefda3 --- /dev/null +++ b/io.openems.common/src/io/openems/common/types/CurrencyConfig.java @@ -0,0 +1,49 @@ +package io.openems.common.types; + +import java.util.Currency; + +/** + * The {@link ChannelId#CURRENCY} mandates the selection of the 'currency' + * configuration property of this specific type. Subsequently, this selected + * property is transformed into the corresponding {@link Currency} type before + * being written through {@link Meta#_setCurrency(Currency)}. + */ +public enum CurrencyConfig { + /** + * Euro. + */ + EUR("€", "Cent", 100f), + /** + * Swedish Krona. + */ + SEK("kr", "Öre", 100f), + /** + * Swiss Francs. + */ + CHF("Fr", "Rappen", 100f); + + private final String symbol; + + private final String underPart; + + private final float ratio; + + private CurrencyConfig(String symbol, String underPart, float ratio) { + this.symbol = symbol; + this.underPart = underPart; + this.ratio = ratio; + } + + public String getSymbol() { + return this.symbol; + } + + public String getUnderPart() { + return this.underPart; + } + + public float getRatio() { + return this.ratio; + } + +} diff --git a/io.openems.common/src/io/openems/common/types/EdgeConfig.java b/io.openems.common/src/io/openems/common/types/EdgeConfig.java index 1a7ad131b23..8014e47cae5 100644 --- a/io.openems.common/src/io/openems/common/types/EdgeConfig.java +++ b/io.openems.common/src/io/openems/common/types/EdgeConfig.java @@ -588,8 +588,7 @@ public static Component fromJson(String componentId, JsonElement json) throws Op var jPropertiesOpt = JsonUtils.getAsOptionalJsonObject(json, "properties"); if (jPropertiesOpt.isPresent()) { for (Entry entry : jPropertiesOpt.get().entrySet()) { - if (!ignorePropertyKey(entry.getKey()) - && !ignoreComponentPropertyKey(componentId, entry.getKey())) { + if (!ignorePropertyKey(entry.getKey())) { properties.put(entry.getKey(), entry.getValue()); } } @@ -1171,9 +1170,9 @@ public TreeMap getFactories() { } /** - * Builds the {@link ActualEdgeConfig}. + * Builds the ActualEdgeConfig. * - * @return {@link ActualEdgeConfig} + * @return ActualEdgeConfig */ public ActualEdgeConfig build() { return new ActualEdgeConfig(ImmutableSortedMap.copyOf(this.getComponents()), @@ -1191,16 +1190,16 @@ public EdgeConfig buildEdgeConfig() { } /** - * Creates an empty {@link ActualEdgeConfig}. + * Creates an empty ActualEdgeConfig. * - * @return {@link ActualEdgeConfig} + * @return ActualEdgeConfig */ public static ActualEdgeConfig empty() { return ActualEdgeConfig.create().build(); } /** - * Create a {@link ActualEdgeConfig.Builder} builder. + * Create a ActualEdgeConfig builder. * * @return a {@link Builder} */ @@ -1248,9 +1247,9 @@ public static EdgeConfig fromJson(JsonObject json) { private volatile JsonObject _json = null; /** - * Build from {@link ActualEdgeConfig}. + * Build from ActualEdgeConfig. * - * @param actual the {@link ActualEdgeConfig} + * @param actual the ActualEdgeConfig */ private EdgeConfig(ActualEdgeConfig actual) { this._actual = actual; @@ -1261,10 +1260,10 @@ private EdgeConfig(JsonObject json) { } /** - * Gets the {@link ActualEdgeConfig}. Either by parsing it from {@link #json} or - * by returning from cache. + * Gets the ActualEdgeConfig. Either by parsing it from {@link #json} or by + * returning from cache. * - * @return {@link ActualEdgeConfig}; empty on JSON parse error + * @return ActualEdgeConfig; empty on JSON parse error */ private synchronized ActualEdgeConfig getActual() { if (this._actual != null) { @@ -1465,25 +1464,4 @@ public static boolean ignorePropertyKey(String key) { default -> false; }; } - - /** - * Internal Method to decide whether a configuration property should be ignored. - * - * @param componentId the Component-ID - * @param key the property key - * @return true if it should get ignored - */ - public static boolean ignoreComponentPropertyKey(String componentId, String key) { - return switch (componentId) { - // Filter for _sum component - case "_sum" -> switch (key) { - case "productionMaxActivePower", "consumptionMaxActivePower", "gridMinActivePower", "gridMaxActivePower" -> - true; - - default -> false; - }; - - default -> false; - }; - } } diff --git a/io.openems.edge.meter.api/src/io/openems/edge/meter/api/MeterType.java b/io.openems.common/src/io/openems/common/types/MeterType.java similarity index 69% rename from io.openems.edge.meter.api/src/io/openems/edge/meter/api/MeterType.java rename to io.openems.common/src/io/openems/common/types/MeterType.java index 7809e7186d3..bb99d4bb8ae 100644 --- a/io.openems.edge.meter.api/src/io/openems/edge/meter/api/MeterType.java +++ b/io.openems.common/src/io/openems/common/types/MeterType.java @@ -1,14 +1,17 @@ -package io.openems.edge.meter.api; +package io.openems.common.types; /** - * Defines the type of the Meter. + * Defines the type of an ElectricityMeter. + * + *

+ * See "io.openems.edge.meter.api" for details. */ public enum MeterType { /** * Defines a Grid-Meter, i.e. a meter that is measuring at the grid connection * point (German: "Netzanschlusspunkt") */ - GRID, // + GRID, /** * Defines a Production-Meter, i.e. a meter that is measuring an electric * producer like a photovoltaics installation @@ -21,8 +24,12 @@ public enum MeterType { */ PRODUCTION_AND_CONSUMPTION, /** - * Defines a Consumption-Meter that is metered, i.e. a meter that is measuring - * an electric consumer like a heating-element or electric car. + * Defines a Consumption-Meter that metered, i.e. a meter that is measuring an + * electric consumer like a heating-element or electric car. + * + *

+ * Select this {@link MeterType} if the device is not actively managed by + * OpenEMS - see {@link #MANAGED_CONSUMPTION_METERED} otherwise. * *

* Note: Consumption is generally calculated using the data from Grid-Meter, @@ -31,6 +38,11 @@ public enum MeterType { * expected to be already measured by the Grid-Meter. */ CONSUMPTION_METERED, + /** + * Defines a Consumption-Meter that is actively managed by OpenEMS and metered + * (See {@link #CONSUMPTION_METERED}). + */ + MANAGED_CONSUMPTION_METERED, /** * Defines a Consumption-Meter that is NOT metered, i.e. a meter that is * measuring an electric consumer like a heating-element or electric car. diff --git a/io.openems.common/src/io/openems/common/utils/ReflectionUtils.java b/io.openems.common/src/io/openems/common/utils/ReflectionUtils.java index fc88d47a09c..ef924855595 100644 --- a/io.openems.common/src/io/openems/common/utils/ReflectionUtils.java +++ b/io.openems.common/src/io/openems/common/utils/ReflectionUtils.java @@ -1,21 +1,134 @@ package io.openems.common.utils; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import io.openems.common.function.ThrowingRunnable; +import io.openems.common.function.ThrowingSupplier; public class ReflectionUtils { + public static class ReflectionException extends RuntimeException { + private static final long serialVersionUID = -8001364348945297741L; + + protected static ReflectionException from(Exception e) { + return new ReflectionException(e.getClass().getSimpleName() + ": " + e.getMessage()); + } + + public ReflectionException(String message) { + super(message); + } + } + private ReflectionUtils() { // no instance needed } + protected static void callGuarded(ThrowingRunnable runnable) throws ReflectionException { + try { + runnable.run(); + } catch (Exception e) { + throw ReflectionException.from(e); + } + } + + protected static T callGuarded(ThrowingSupplier supplier) throws ReflectionException { + try { + return supplier.get(); + } catch (Exception e) { + throw ReflectionException.from(e); + } + } + + /** + * Sets the value of a Field via Java Reflection. + * + * @param object the target object + * @param memberName the name the declared field + * @param value the value to be set + * @throws Exception on error + */ + public static void setAttributeViaReflection(Object object, String memberName, Object value) + throws ReflectionException { + var field = getField(object.getClass(), memberName); + callGuarded(() -> field.set(object, value)); + } + + /** + * Sets the value of a static Field via Java Reflection. + * + * @param clazz the {@link Class} + * @param memberName the name the declared field + * @param value the value to be set + * @throws Exception on error + */ + public static void setStaticAttributeViaReflection(Class clazz, String memberName, Object value) + throws ReflectionException { + var field = getField(clazz, memberName); + callGuarded(() -> field.set(null, value)); + } + + /** + * Gets the value of a Field via Java Reflection. + * + * @param the type of the value + * @param object the target object + * @param memberName the name the declared field + * @return the value + * @throws Exception on error + */ @SuppressWarnings("unchecked") - public static boolean setAttribute(Class clazz, T object, String memberName, Object value) - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public static T getValueViaReflection(Object object, String memberName) throws ReflectionException { + var field = getField(object.getClass(), memberName); + return (T) callGuarded(() -> field.get(object)); + } + + /** + * Invokes a {@link Method} that takes no arguments via Java Reflection. + * + * @param the type of the result + * @param object the target object + * @param memberName the name of the method + * @return the result of the method + * @throws Exception on error + */ + public static T invokeMethodWithoutArgumentsViaReflection(Object object, String memberName) + throws ReflectionException { + var method = callGuarded(() -> object.getClass().getDeclaredMethod(memberName)); + return invokeMethodViaReflection(object, method); + } + + /** + * Invokes a {@link Method} via Java Reflection. + * + * @param the type of the result + * @param object the target object + * @param method the {@link Method} + * @param args the arguments to be set + * @return the result of the method + * @throws Exception on error + */ + @SuppressWarnings("unchecked") + public static T invokeMethodViaReflection(Object object, Method method, Object... args) + throws ReflectionException { + method.setAccessible(true); + return (T) callGuarded(() -> method.invoke(object, args)); + } + + /** + * Gets the {@link Class#getDeclaredField(String)} in the given {@link Class} or + * any of its superclasses. + * + * @param clazz the given {@link Class} + * @param memberName the name of the declared field + * @return a {@link Field} + * @throws ReflectionException if there is no such field + */ + public static Field getField(Class clazz, String memberName) throws ReflectionException { try { var field = clazz.getDeclaredField(memberName); field.setAccessible(true); - field.set(object, value); - return true; + return field; } catch (NoSuchFieldException e) { // Ignore. } @@ -23,9 +136,8 @@ public static boolean setAttribute(Class clazz, T object, Strin // classes. Class parent = clazz.getSuperclass(); if (parent == null) { - return false; // reached 'java.lang.Object' + throw new ReflectionException("Reached java.lang.Object"); } - return setAttribute((Class) parent, object, memberName, value); + return getField(parent, memberName); } - } diff --git a/io.openems.common/src/io/openems/common/utils/XmlUtils.java b/io.openems.common/src/io/openems/common/utils/XmlUtils.java index b921670e906..0a81ca749b8 100644 --- a/io.openems.common/src/io/openems/common/utils/XmlUtils.java +++ b/io.openems.common/src/io/openems/common/utils/XmlUtils.java @@ -1,5 +1,7 @@ package io.openems.common.utils; +import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -7,9 +9,15 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + import org.w3c.dom.DOMException; +import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; import io.openems.common.exceptions.OpenemsError; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; @@ -254,4 +262,26 @@ public static Stream stream(final Node node) { var childNodes = node.getChildNodes(); return IntStream.range(0, childNodes.getLength()).boxed().map(childNodes::item); } + + /** + * Parses the provided XML string and returns the root {@link Element} of the + * XML document. + * + * @param xml the XML string to parse + * @return the root {@link Element} of the parsed XML document + * @throws ParserConfigurationException if a DocumentBuilder cannot be created + * which satisfies the configuration + * requested + * @throws SAXException if any parse errors occur while + * processing the XML + * @throws IOException if an I/O error occurs during parsing + */ + public static Element getXmlRootDocument(String xml) + throws ParserConfigurationException, SAXException, IOException { + var dbFactory = DocumentBuilderFactory.newInstance(); + var dBuilder = dbFactory.newDocumentBuilder(); + var is = new InputSource(new StringReader(xml)); + var doc = dBuilder.parse(is); + return doc.getDocumentElement(); + } } diff --git a/io.openems.common/src/io/openems/common/websocket/AbstractWebsocket.java b/io.openems.common/src/io/openems/common/websocket/AbstractWebsocket.java index c76b34dd560..f313c175896 100644 --- a/io.openems.common/src/io/openems/common/websocket/AbstractWebsocket.java +++ b/io.openems.common/src/io/openems/common/websocket/AbstractWebsocket.java @@ -128,11 +128,17 @@ protected final boolean sendMessage(WebSocket ws, JsonrpcMessage message) { } private void sendMessageFailedLog(WebSocket ws, JsonrpcMessage message) { - this.logWarn(this.log, new StringBuilder() // - .append("[").append(generateWsDataString(ws)) // - .append("] Unable to send message: Connection is closed. ") // - .append(toShortString(simplifyJsonrpcMessage(message), 100)) // - .toString()); + final var b = new StringBuilder(); + + var wsDataString = generateWsDataString(ws); + if (!wsDataString.isEmpty()) { + b.append("[").append(generateWsDataString(ws)).append("] "); + } + + this.logWarn(this.log, // + b.append("Unable to send message: Connection is closed. ") // + .append(toShortString(simplifyJsonrpcMessage(message), 200)) // + .toString()); } /** diff --git a/io.openems.common/src/io/openems/common/websocket/DummyWebsocketServer.java b/io.openems.common/src/io/openems/common/websocket/DummyWebsocketServer.java index b815deeb71c..f4b2b6cf916 100644 --- a/io.openems.common/src/io/openems/common/websocket/DummyWebsocketServer.java +++ b/io.openems.common/src/io/openems/common/websocket/DummyWebsocketServer.java @@ -93,12 +93,7 @@ private DummyWebsocketServer(DummyWebsocketServer.Builder builder) { @Override protected WsData createWsData(WebSocket ws) { - return new WsData(ws) { - @Override - public String toString() { - return "DummyWebsocketServer.WsData []"; - } - }; + return new WsData(ws); } @Override diff --git a/io.openems.common/src/io/openems/common/websocket/MyDraft6455.java b/io.openems.common/src/io/openems/common/websocket/MyDraft6455.java index 3f549c9cb49..4c4968114c6 100644 --- a/io.openems.common/src/io/openems/common/websocket/MyDraft6455.java +++ b/io.openems.common/src/io/openems/common/websocket/MyDraft6455.java @@ -23,6 +23,7 @@ import org.java_websocket.enums.Opcode; import org.java_websocket.enums.ReadyState; import org.java_websocket.enums.Role; +import org.java_websocket.exceptions.IncompleteException; import org.java_websocket.exceptions.InvalidDataException; import org.java_websocket.exceptions.InvalidFrameException; import org.java_websocket.exceptions.InvalidHandshakeException; diff --git a/io.openems.common/src/io/openems/common/websocket/WebsocketUtils.java b/io.openems.common/src/io/openems/common/websocket/WebsocketUtils.java index e9f51ae5b04..cb80dc12b69 100644 --- a/io.openems.common/src/io/openems/common/websocket/WebsocketUtils.java +++ b/io.openems.common/src/io/openems/common/websocket/WebsocketUtils.java @@ -53,11 +53,11 @@ public static String parseRemoteIdentifier(WebSocket ws, Handshakedata handshake } /** - * Gets the toString() content of the WsData attachment of the WebSocket; or + * Gets the toLogString() content of the WsData attachment of the WebSocket; or * empty string if not available. * * @param ws the WebSocket - * @return the {@link WsData#toString()} content + * @return the {@link WsData#toLogString()} content */ public static String generateWsDataString(WebSocket ws) { if (ws == null) { @@ -67,6 +67,10 @@ public static String generateWsDataString(WebSocket ws) { if (wsData == null) { return ""; } - return wsData.toString(); + var logString = wsData.toLogString(); + if (logString == null) { + return ""; + } + return logString; } } diff --git a/io.openems.common/src/io/openems/common/websocket/WsData.java b/io.openems.common/src/io/openems/common/websocket/WsData.java index b43aedad7f7..852b4290125 100644 --- a/io.openems.common/src/io/openems/common/websocket/WsData.java +++ b/io.openems.common/src/io/openems/common/websocket/WsData.java @@ -21,14 +21,14 @@ * Objects of this class are used to store additional data with websocket * connections of WebSocketClient and WebSocketServer. */ -public abstract class WsData { +public class WsData { /** * Holds the WebSocket. */ private final WebSocket websocket; - protected WsData(WebSocket ws) { + public WsData(WebSocket ws) { this.websocket = ws; } @@ -138,10 +138,11 @@ public void handleJsonrpcResponse(JsonrpcResponse response) throws OpenemsNamedE } /** - * Provides a specific toString method. + * Provides a specific log string. * * @return a specific string for this instance */ - @Override - public abstract String toString(); + protected String toLogString() { + return ""; + } } diff --git a/io.openems.common/src/io/openems/common/worker/AbstractWorker.java b/io.openems.common/src/io/openems/common/worker/AbstractWorker.java index 29b371fd99b..c78b265fb2b 100644 --- a/io.openems.common/src/io/openems/common/worker/AbstractWorker.java +++ b/io.openems.common/src/io/openems/common/worker/AbstractWorker.java @@ -65,9 +65,7 @@ public void activate(String name) { * false */ public void modified(String name, boolean initiallyTriggerNextRun) { - if (!this.thread.isAlive() && !this.thread.isInterrupted() && !this.isStopped.get()) { - this.startWorker(name, initiallyTriggerNextRun); - } + this.startWorker(name, initiallyTriggerNextRun); } /** @@ -79,12 +77,13 @@ public void modified(String name) { this.modified(name, true); } - private void startWorker(String name, boolean autoTriggerNextRun) { + private synchronized void startWorker(String name, boolean autoTriggerNextRun) { if (name != null) { this.thread.setName(name); } - this.thread.start(); - + if (!this.thread.isAlive() && !this.thread.isInterrupted() && !this.isStopped.get()) { + this.thread.start(); + } if (autoTriggerNextRun) { this.triggerNextRun(); } diff --git a/io.openems.common/test/io/openems/common/jsonrpc/response/CreateXlxsTest.java b/io.openems.common/test/io/openems/common/jsonrpc/response/CreateXlxsTest.java new file mode 100644 index 00000000000..f855b102bee --- /dev/null +++ b/io.openems.common/test/io/openems/common/jsonrpc/response/CreateXlxsTest.java @@ -0,0 +1,248 @@ +package io.openems.common.jsonrpc.response; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Base64; +import java.util.EnumMap; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.UUID; +import java.util.function.Consumer; + +import com.google.common.collect.ImmutableSortedMap; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesExportXlsxResponse.Channel; +import io.openems.common.session.Language; +import io.openems.common.timedata.XlsxExportDetailData; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportCategory; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportDataEntry; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportDataEntry.HistoricTimedataSaveType; +import io.openems.common.types.ChannelAddress; +import io.openems.common.types.CurrencyConfig; + +public class CreateXlxsTest { + + private static SortedMap getMockedEnergyData() { + return ImmutableSortedMap.naturalOrder() // + .put(Channel.GRID_BUY_ACTIVE_ENERGY, new JsonPrimitive(500)) // + .put(Channel.GRID_SELL_ACTIVE_ENERGY, new JsonPrimitive(0)) // + .put(Channel.PRODUCTION_ACTIVE_ENERGY, new JsonPrimitive(300)) // + .put(Channel.CONSUMPTION_ACTIVE_ENERGY, new JsonPrimitive(700)) // + .put(Channel.ESS_DC_CHARGE_ENERGY, new JsonPrimitive(100)) // + .put(Channel.ESS_DC_DISCHARGE_ENERGY, new JsonPrimitive(80)) // + .build(); + } + + private static SortedMap> getMockedPowerData() { + + SortedMap values = new TreeMap<>(); + values.put(Channel.GRID_ACTIVE_POWER, new JsonPrimitive(50)); + values.put(Channel.PRODUCTION_ACTIVE_POWER, new JsonPrimitive(50)); + values.put(Channel.CONSUMPTION_ACTIVE_POWER, new JsonPrimitive(50)); + values.put(Channel.ESS_DISCHARGE_POWER, new JsonPrimitive(50)); + values.put(Channel.ESS_SOC, new JsonPrimitive(50)); + values.put(new ChannelAddress("meter0", "ActivePower"), new JsonPrimitive(100)); + values.put(new ChannelAddress("meter1", "ActivePower"), new JsonPrimitive(412)); + values.put(new ChannelAddress("evcs0", "ChargePower"), new JsonPrimitive(75)); + values.put(new ChannelAddress("meter2", "ActivePower"), new JsonPrimitive(10)); + values.put(new ChannelAddress("_sum", "GridBuyPower"), new JsonPrimitive(292.5)); + + return ImmutableSortedMap.>naturalOrder() + .put(ZonedDateTime.of(2020, 07, 01, 0, 15, 0, 0, ZoneId.systemDefault()).plusMinutes(15), values) // + .put(ZonedDateTime.of(2020, 07, 01, 0, 30, 0, 0, ZoneId.systemDefault()).plusMinutes(15), values) // + .put(ZonedDateTime.of(2020, 07, 01, 0, 45, 0, 0, ZoneId.systemDefault()).plusMinutes(15), values) // + .put(ZonedDateTime.of(2020, 07, 01, 1, 0, 0, 0, ZoneId.systemDefault()).plusMinutes(15), values) // + .put(ZonedDateTime.of(2020, 07, 01, 1, 15, 0, 0, ZoneId.systemDefault()).plusMinutes(15), values) // + .put(ZonedDateTime.of(2020, 07, 01, 1, 30, 0, 0, ZoneId.systemDefault()).plusMinutes(15), values) // + .build(); + } + + private static XlsxExportDetailData getMockedDetailData() { + final var enumMap = new EnumMap>(XlsxExportCategory.class); + final var consumption = new ArrayList(); + final var production = new ArrayList(); + final var tou = new ArrayList(); + + enumMap.put(XlsxExportCategory.PRODUCTION, production); + enumMap.put(XlsxExportCategory.CONSUMPTION, consumption); + enumMap.put(XlsxExportCategory.TIME_OF_USE_TARIFF, tou); + + production.add(new XlsxExportDataEntry("PV-Dach", new ChannelAddress("meter0", "ActivePower"), + HistoricTimedataSaveType.POWER)); + production.add(new XlsxExportDataEntry("PV-Alm", new ChannelAddress("meter1", "ActivePower"), + HistoricTimedataSaveType.POWER)); + consumption.add(new XlsxExportDataEntry("Consumption Meter", new ChannelAddress("meter2", "ActivePower"), + HistoricTimedataSaveType.POWER)); + consumption.add(new XlsxExportDataEntry("Wallbox Garage", new ChannelAddress("evcs0", "ChargePower"), + HistoricTimedataSaveType.POWER)); + tou.add(new XlsxExportDataEntry("Dynamisch Gut", new ChannelAddress("_sum", "GridBuyPower"), + HistoricTimedataSaveType.POWER)); + + return new XlsxExportDetailData(enumMap, CurrencyConfig.EUR); + } + + /** + * Main Method for creating a excel export with mocked data. + * + * @param args not used + * @throws IOException if file cant be written + * @throws OpenemsNamedException requests fails + */ + public static void main(String[] args) throws IOException, OpenemsNamedException { + createFullXlsx(); + createHalfXlsx(); + createConsumptionOnlyXlsx(); + createProductionOnlyXlsx(); + createTouOnlyXlsx(); + createProductionAndTouXlsx(); + createConsumptionAndTouXlsx(); + createnSingleOfAllXlsx(); + } + + private static void createFullXlsx() throws IOException, OpenemsNamedException { + var fromDate = ZonedDateTime.of(2020, 07, 01, 0, 0, 0, 0, ZoneId.systemDefault()); + var toDate = ZonedDateTime.of(2020, 07, 02, 0, 0, 0, 0, ZoneId.systemDefault()); + + var powerData = CreateXlxsTest.getMockedPowerData(); + var energyData = CreateXlxsTest.getMockedEnergyData(); + var detailData = CreateXlxsTest.getMockedDetailData(); + + final var request = new QueryHistoricTimeseriesExportXlsxResponse(UUID.randomUUID(), "edge0", fromDate, toDate, + powerData, energyData, Language.EN, detailData); + + var payload = request.getPayload(); + + byte[] excelData = Base64.getDecoder().decode(payload); + + String filePath = ".\\..\\build\\fullTestPrint.xlsx"; + + try (FileOutputStream fos = new FileOutputStream(filePath)) { + fos.write(excelData); + System.out.println("Testfile created under: " + filePath); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void createHalfXlsx() throws IOException, OpenemsNamedException { + createTestPrint(".\\..\\build\\emptyTestPrint.xlsx", null, null, null); + } + + private static void createTestPrint(String filePath, Consumer> consProd, + Consumer> consCons, Consumer> consTou) + throws IOException, OpenemsNamedException { + final var enumMap = new EnumMap>(XlsxExportCategory.class); + final var consumption = new ArrayList(); + final var production = new ArrayList(); + final var tou = new ArrayList(); + + if (consProd != null) { + consProd.accept(production); + } + + if (consCons != null) { + consCons.accept(consumption); + } + + if (consTou != null) { + consTou.accept(tou); + } + + enumMap.put(XlsxExportCategory.PRODUCTION, production); + enumMap.put(XlsxExportCategory.CONSUMPTION, consumption); + enumMap.put(XlsxExportCategory.TIME_OF_USE_TARIFF, tou); + + var detailData = new XlsxExportDetailData(enumMap, CurrencyConfig.EUR); + + var fromDate = ZonedDateTime.of(2020, 07, 01, 0, 0, 0, 0, ZoneId.systemDefault()); + var toDate = ZonedDateTime.of(2020, 07, 02, 0, 0, 0, 0, ZoneId.systemDefault()); + + var powerData = CreateXlxsTest.getMockedPowerData(); + var energyData = CreateXlxsTest.getMockedEnergyData(); + + final var request = new QueryHistoricTimeseriesExportXlsxResponse(UUID.randomUUID(), "edge0", fromDate, toDate, + powerData, energyData, Language.EN, detailData); + + var payload = request.getPayload(); + + byte[] excelData = Base64.getDecoder().decode(payload); + + try (FileOutputStream fos = new FileOutputStream(filePath)) { + fos.write(excelData); + System.out.println("Testfile created under: " + filePath); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void createProductionOnlyXlsx() throws IOException, OpenemsNamedException { + createTestPrint(".\\..\\build\\prodTestPrint.xlsx", production -> { + production.add(new XlsxExportDataEntry("PV-Dach", new ChannelAddress("meter0", "ActivePower"), + HistoricTimedataSaveType.POWER)); + production.add(new XlsxExportDataEntry("PV-Alm", new ChannelAddress("meter1", "ActivePower"), + HistoricTimedataSaveType.POWER)); + }, null, null); + } + + private static void createConsumptionOnlyXlsx() throws IOException, OpenemsNamedException { + createTestPrint(".\\..\\build\\consTestPrint.xlsx", null, consumption -> { + consumption.add(new XlsxExportDataEntry("Consumption Meter", new ChannelAddress("meter2", "ActivePower"), + HistoricTimedataSaveType.POWER)); + consumption.add(new XlsxExportDataEntry("Wallbox Garage", new ChannelAddress("evcs0", "ChargePower"), + HistoricTimedataSaveType.POWER)); + }, null); + } + + private static void createTouOnlyXlsx() throws IOException, OpenemsNamedException { + createTestPrint(".\\..\\build\\touPrint.xlsx", null, null, tou -> { + tou.add(new XlsxExportDataEntry("Dynamisch Gut", new ChannelAddress("_sum", "GridBuyPower"), + HistoricTimedataSaveType.POWER)); + }); + } + + private static void createProductionAndTouXlsx() throws IOException, OpenemsNamedException { + createTestPrint(".\\..\\build\\prodAndTouPrint.xlsx", production -> { + production.add(new XlsxExportDataEntry("PV-Dach", new ChannelAddress("meter0", "ActivePower"), + HistoricTimedataSaveType.POWER)); + production.add(new XlsxExportDataEntry("PV-Alm", new ChannelAddress("meter1", "ActivePower"), + HistoricTimedataSaveType.POWER)); + }, null, tou -> { + tou.add(new XlsxExportDataEntry("Dynamisch Gut", new ChannelAddress("_sum", "GridBuyPower"), + HistoricTimedataSaveType.POWER)); + }); + + } + + private static void createConsumptionAndTouXlsx() throws IOException, OpenemsNamedException { + createTestPrint(".\\..\\build\\consAndTouPrint.xlsx", null, consumption -> { + consumption.add(new XlsxExportDataEntry("Consumption Meter", new ChannelAddress("meter2", "ActivePower"), + HistoricTimedataSaveType.POWER)); + consumption.add(new XlsxExportDataEntry("Wallbox Garage", new ChannelAddress("evcs0", "ChargePower"), + HistoricTimedataSaveType.POWER)); + }, tou -> { + tou.add(new XlsxExportDataEntry("Dynamisch Gut", new ChannelAddress("_sum", "GridBuyPower"), + HistoricTimedataSaveType.POWER)); + }); + } + + private static void createnSingleOfAllXlsx() throws IOException, OpenemsNamedException { + createTestPrint(".\\..\\build\\singleOfAllPrint.xlsx", production -> { + production.add(new XlsxExportDataEntry("PV-Alm", new ChannelAddress("meter1", "ActivePower"), + HistoricTimedataSaveType.POWER)); + }, consumption -> { + consumption.add(new XlsxExportDataEntry("Wallbox Garage", new ChannelAddress("evcs0", "ChargePower"), + HistoricTimedataSaveType.POWER)); + }, tou -> { + tou.add(new XlsxExportDataEntry("Dynamisch Gut", new ChannelAddress("_sum", "GridBuyPower"), + HistoricTimedataSaveType.POWER)); + }); + + } +} diff --git a/io.openems.common/test/io/openems/common/timedata/TimeoutTest.java b/io.openems.common/test/io/openems/common/timedata/TimeoutTest.java new file mode 100644 index 00000000000..15ef3d8b49d --- /dev/null +++ b/io.openems.common/test/io/openems/common/timedata/TimeoutTest.java @@ -0,0 +1,27 @@ +package io.openems.common.timedata; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.temporal.ChronoUnit; + +import org.junit.Test; + +import io.openems.common.test.TimeLeapClock; + +public class TimeoutTest { + + @Test + public void test() { + final var timeout = Timeout.ofSeconds(120); + final var timeLeap = new TimeLeapClock(); + timeout.start(timeLeap); + + timeLeap.leap(20, ChronoUnit.SECONDS); + assertFalse(timeout.elapsed(timeLeap)); + + timeLeap.leap(121, ChronoUnit.SECONDS); + assertTrue(timeout.elapsed(timeLeap)); + } + +} diff --git a/io.openems.common/test/io/openems/common/timedata/XlsxExportUtilTest.java b/io.openems.common/test/io/openems/common/timedata/XlsxExportUtilTest.java new file mode 100644 index 00000000000..1328f3a4ff9 --- /dev/null +++ b/io.openems.common/test/io/openems/common/timedata/XlsxExportUtilTest.java @@ -0,0 +1,92 @@ +package io.openems.common.timedata; + +import static io.openems.common.utils.JsonUtils.toJson; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.google.common.collect.ImmutableSortedMap; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportCategory; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportDataEntry.HistoricTimedataSaveType; +import io.openems.common.types.EdgeConfig.ActualEdgeConfig; +import io.openems.common.types.EdgeConfig.Component; +import io.openems.common.types.EdgeConfig.Factory; +import io.openems.common.types.EdgeConfig.Factory.Property; + +public class XlsxExportUtilTest { + + @Test + public void testGetDetailData() throws OpenemsNamedException { + var edgeConfig = ActualEdgeConfig.create() // + .addComponent("meter0", + new Component("meter0", "My CONSUMPTION_METERED Meter", "Meter.Socomec.Threephase", + // Properties + ImmutableSortedMap.of("type", toJson("CONSUMPTION_METERED")), + // Channels + ImmutableSortedMap.of())) // + .addComponent("meter1", + new Component("meter1", "My CONSUMPTION_NOT_METERED Meter", "Meter.Socomec.Threephase", + // Properties + ImmutableSortedMap.of("type", toJson("CONSUMPTION_NOT_METERED")), + // Channels + ImmutableSortedMap.of())) // + .addComponent("meter2", new Component("meter2", "My PRODUCTION Meter", "Meter.Socomec.Threephase", + // Properties + ImmutableSortedMap.of("type", toJson("PRODUCTION")), + // Channels + ImmutableSortedMap.of())) // + .addComponent("meter3", + new Component("meter3", "My MANAGED_CONSUMPTION_METERED Meter", "Meter.Socomec.Threephase", + // Properties + ImmutableSortedMap.of("type", toJson("MANAGED_CONSUMPTION_METERED")), + // Channels + ImmutableSortedMap.of())) // + + .addFactory("Meter.Socomec.Threephase", + new Factory("Meter.Socomec.Threephase", "My Name", "My Description", // + new Property[] {}, // + // Natures + new String[] { "io.openems.edge.meter.api.ElectricityMeter" })) // + .buildEdgeConfig(); + + final var result = XlsxExportUtil.getDetailData(edgeConfig); + + var consumptions = result.data().get(XlsxExportCategory.CONSUMPTION); + assertEquals(3, consumptions.size()); + + { + var meter = consumptions.get(0); + assertEquals("My CONSUMPTION_METERED Meter", meter.alias()); + assertEquals("meter0/ActivePower", meter.channel().toString()); + assertEquals(HistoricTimedataSaveType.POWER, meter.type()); + } + { + var meter = consumptions.get(1); + assertEquals("My CONSUMPTION_NOT_METERED Meter", meter.alias()); + assertEquals("meter1/ActivePower", meter.channel().toString()); + assertEquals(HistoricTimedataSaveType.POWER, meter.type()); + } + { + var meter = consumptions.get(2); + assertEquals("My MANAGED_CONSUMPTION_METERED Meter", meter.alias()); + assertEquals("meter3/ActivePower", meter.channel().toString()); + assertEquals(HistoricTimedataSaveType.POWER, meter.type()); + } + + var productions = result.data().get(XlsxExportCategory.PRODUCTION); + assertEquals(1, productions.size()); + + { + var meter = productions.get(0); + assertEquals("My PRODUCTION Meter", meter.alias()); + assertEquals("meter2/ActivePower", meter.channel().toString()); + assertEquals(HistoricTimedataSaveType.POWER, meter.type()); + } + + var touts = result.data().get(XlsxExportCategory.TIME_OF_USE_TARIFF); + assertEquals(0, touts.size()); + } + +} diff --git a/io.openems.common/test/io/openems/common/websocket/ClientReconnectorWorkerTest.java b/io.openems.common/test/io/openems/common/websocket/ClientReconnectorWorkerTest.java index 3eb4161bd07..62373bfe85a 100644 --- a/io.openems.common/test/io/openems/common/websocket/ClientReconnectorWorkerTest.java +++ b/io.openems.common/test/io/openems/common/websocket/ClientReconnectorWorkerTest.java @@ -9,28 +9,15 @@ public class ClientReconnectorWorkerTest { - private static class MyWsData extends WsData { - - public MyWsData(WebSocket ws) { - super(ws); - } - - @Override - public String toString() { - return ""; - } - - } - - private static class MyWebsocketClient extends AbstractWebsocketClient { + private static class MyWebsocketClient extends AbstractWebsocketClient { public MyWebsocketClient(String name, URI serverUri) { super(name, serverUri); } @Override - protected MyWsData createWsData(WebSocket ws) { - return new MyWsData(ws); + protected WsData createWsData(WebSocket ws) { + return new WsData(ws); } @Override diff --git a/io.openems.common/test/io/openems/common/websocket/DummyWebsocketServerTest.java b/io.openems.common/test/io/openems/common/websocket/DummyWebsocketServerTest.java new file mode 100644 index 00000000000..172d3516be5 --- /dev/null +++ b/io.openems.common/test/io/openems/common/websocket/DummyWebsocketServerTest.java @@ -0,0 +1,15 @@ +package io.openems.common.websocket; + +import org.junit.Test; + +public class DummyWebsocketServerTest { + + @Test + public void test() { + var sut = DummyWebsocketServer.create() // + .build(); + sut.createWsData(null); + sut.stop(); + } + +} diff --git a/io.openems.common/test/io/openems/common/websocket/WebsocketUtilsTest.java b/io.openems.common/test/io/openems/common/websocket/WebsocketUtilsTest.java new file mode 100644 index 00000000000..25febb897d4 --- /dev/null +++ b/io.openems.common/test/io/openems/common/websocket/WebsocketUtilsTest.java @@ -0,0 +1,15 @@ +package io.openems.common.websocket; + +import static io.openems.common.websocket.WebsocketUtils.generateWsDataString; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class WebsocketUtilsTest { + + @Test + public void test() { + assertEquals("", generateWsDataString(null)); + } + +} diff --git a/io.openems.common/test/io/openems/common/websocket/WsDataTest.java b/io.openems.common/test/io/openems/common/websocket/WsDataTest.java new file mode 100644 index 00000000000..5dd1a7aad2f --- /dev/null +++ b/io.openems.common/test/io/openems/common/websocket/WsDataTest.java @@ -0,0 +1,17 @@ +package io.openems.common.websocket; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class WsDataTest { + + @Test + public void test() { + var sut = new WsData(null); + assertEquals("", sut.toLogString()); + + sut.dispose(); + } + +} diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 11442b68bce..218d2542807 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -74,6 +74,7 @@ bnd.identity;id='io.openems.edge.controller.ess.delaycharge',\ bnd.identity;id='io.openems.edge.controller.ess.delayedselltogrid',\ bnd.identity;id='io.openems.edge.controller.ess.emergencycapacityreserve',\ + bnd.identity;id='io.openems.edge.controller.ess.fastfrequencyreserve',\ bnd.identity;id='io.openems.edge.controller.ess.fixactivepower',\ bnd.identity;id='io.openems.edge.controller.ess.fixstateofcharge',\ bnd.identity;id='io.openems.edge.controller.ess.gridoptimizedcharge',\ @@ -189,13 +190,14 @@ bnd.identity;id='io.openems.edge.timeofusetariff.groupe',\ bnd.identity;id='io.openems.edge.timeofusetariff.hassfurt',\ bnd.identity;id='io.openems.edge.timeofusetariff.rabotcharge',\ + bnd.identity;id='io.openems.edge.timeofusetariff.swisspower',\ bnd.identity;id='io.openems.edge.timeofusetariff.tibber',\ -runbundles: \ Java-WebSocket;version='[1.5.4,1.5.5)',\ - bcpkix;version='[1.70.0,1.70.1)',\ - bcprov;version='[1.70.0,1.70.1)',\ - bcutil;version='[1.70.0,1.70.1)',\ + bcpkix;version='[1.78.1,1.78.2)',\ + bcprov;version='[1.78.1,1.78.2)',\ + bcutil;version='[1.78.1,1.78.2)',\ com.fasterxml.aalto-xml;version='[1.3.3,1.3.4)',\ com.fazecast.jSerialComm;version='[2.10.4,2.10.5)',\ com.ghgande.j2mod;version='[3.2.1,3.2.2)',\ @@ -245,6 +247,7 @@ io.openems.edge.controller.ess.delaycharge;version=snapshot,\ io.openems.edge.controller.ess.delayedselltogrid;version=snapshot,\ io.openems.edge.controller.ess.emergencycapacityreserve;version=snapshot,\ + io.openems.edge.controller.ess.fastfrequencyreserve;version=snapshot,\ io.openems.edge.controller.ess.fixactivepower;version=snapshot,\ io.openems.edge.controller.ess.fixstateofcharge;version=snapshot,\ io.openems.edge.controller.ess.gridoptimizedcharge;version=snapshot,\ @@ -371,6 +374,7 @@ io.openems.edge.timeofusetariff.groupe;version=snapshot,\ io.openems.edge.timeofusetariff.hassfurt;version=snapshot,\ io.openems.edge.timeofusetariff.rabotcharge;version=snapshot,\ + io.openems.edge.timeofusetariff.swisspower;version=snapshot,\ io.openems.edge.timeofusetariff.tibber;version=snapshot,\ io.openems.oem.openems;version=snapshot,\ io.openems.shared.influxdb;version=snapshot,\ @@ -416,7 +420,7 @@ org.eclipse.jetty.io;version='[9.4.28,9.4.29)',\ org.eclipse.jetty.util;version='[9.4.28,9.4.29)',\ org.eclipse.paho.mqttv5.client;version='[1.2.5,1.2.6)',\ - org.jetbrains.kotlin.osgi-bundle;version='[2.0.20,2.0.21)',\ + org.jetbrains.kotlin.osgi-bundle;version='[2.0.21,2.0.22)',\ org.jsoup;version='[1.18.1,1.18.2)',\ org.jsr-305;version='[3.0.2,3.0.3)',\ org.openmuc.jmbus;version='[3.3.0,3.3.1)',\ diff --git a/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java index de029ad3ba5..74b3d0f4895 100644 --- a/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java +++ b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java @@ -1,14 +1,24 @@ package io.openems.edge.battery.protection; +import static io.openems.edge.battery.api.Battery.ChannelId.CHARGE_MAX_CURRENT; +import static io.openems.edge.battery.api.Battery.ChannelId.DISCHARGE_MAX_CURRENT; +import static io.openems.edge.battery.api.Battery.ChannelId.MAX_CELL_TEMPERATURE; +import static io.openems.edge.battery.api.Battery.ChannelId.MAX_CELL_VOLTAGE; +import static io.openems.edge.battery.api.Battery.ChannelId.MIN_CELL_TEMPERATURE; +import static io.openems.edge.battery.api.Battery.ChannelId.MIN_CELL_VOLTAGE; +import static io.openems.edge.battery.protection.BatteryProtection.ChannelId.BP_CHARGE_BMS; +import static io.openems.edge.battery.protection.BatteryProtection.ChannelId.BP_DISCHARGE_BMS; +import static io.openems.edge.common.startstop.StartStoppable.ChannelId.START_STOP; +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.SECONDS; + import java.time.Instant; import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; import org.junit.Test; import io.openems.common.channel.Unit; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; import io.openems.edge.battery.protection.currenthandler.ChargeMaxCurrentHandler; import io.openems.edge.battery.protection.currenthandler.DischargeMaxCurrentHandler; @@ -18,7 +28,6 @@ import io.openems.edge.common.channel.Doc; import io.openems.edge.common.linecharacteristic.PolyLine; import io.openems.edge.common.startstop.StartStop; -import io.openems.edge.common.startstop.StartStoppable; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; @@ -102,22 +111,6 @@ public Doc doc() { private static final String BATTERY_ID = "battery0"; - private static final ChannelAddress BATTERY_START_STOP = new ChannelAddress(BATTERY_ID, - StartStoppable.ChannelId.START_STOP.id()); - private static final ChannelAddress BATTERY_BP_CHARGE_BMS = new ChannelAddress(BATTERY_ID, - BatteryProtection.ChannelId.BP_CHARGE_BMS.id()); - private static final ChannelAddress BATTERY_BP_DISCHARGE_BMS = new ChannelAddress(BATTERY_ID, - BatteryProtection.ChannelId.BP_DISCHARGE_BMS.id()); - private static final ChannelAddress BATTERY_MIN_CELL_VOLTAGE = new ChannelAddress(BATTERY_ID, "MinCellVoltage"); - private static final ChannelAddress BATTERY_MAX_CELL_VOLTAGE = new ChannelAddress(BATTERY_ID, "MaxCellVoltage"); - private static final ChannelAddress BATTERY_MIN_CELL_TEMPERATURE = new ChannelAddress(BATTERY_ID, - "MinCellTemperature"); - private static final ChannelAddress BATTERY_MAX_CELL_TEMPERATURE = new ChannelAddress(BATTERY_ID, - "MaxCellTemperature"); - private static final ChannelAddress BATTERY_CHARGE_MAX_CURRENT = new ChannelAddress(BATTERY_ID, "ChargeMaxCurrent"); - private static final ChannelAddress BATTERY_DISCHARGE_MAX_CURRENT = new ChannelAddress(BATTERY_ID, - "DischargeMaxCurrent"); - @Test public void test() throws Exception { final var battery = new DummyBattery(BATTERY_ID); @@ -137,169 +130,168 @@ public void test() throws Exception { .setForceCharge(FORCE_CHARGE) // .build()) // .build(); - new ComponentTest(new DummyBattery(BATTERY_ID)) // - .addComponent(battery) // + new ComponentTest(battery) // .next(new TestCase() // - .input(BATTERY_START_STOP, StartStop.START) // - .input(BATTERY_BP_CHARGE_BMS, 80) // - .input(BATTERY_BP_DISCHARGE_BMS, 80) // - .input(BATTERY_MIN_CELL_VOLTAGE, 2950) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3300) // - .input(BATTERY_MIN_CELL_TEMPERATURE, 16) // - .input(BATTERY_MAX_CELL_TEMPERATURE, 17) // + .input(START_STOP, StartStop.START) // + .input(BP_CHARGE_BMS, 80) // + .input(BP_DISCHARGE_BMS, 80) // + .input(MIN_CELL_VOLTAGE, 2950) // + .input(MAX_CELL_VOLTAGE, 3300) // + .input(MIN_CELL_TEMPERATURE, 16) // + .input(MAX_CELL_TEMPERATURE, 17) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 0)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 0)) // .next(new TestCase("open, but maxIncreaseAmpereLimit") // - .timeleap(clock, 2, ChronoUnit.SECONDS) // - .input(BATTERY_MIN_CELL_VOLTAGE, 3000) // + .timeleap(clock, 2, SECONDS) // + .input(MIN_CELL_VOLTAGE, 3000) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 1) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 1)) // + .output(CHARGE_MAX_CURRENT, 1) // + .output(DISCHARGE_MAX_CURRENT, 1)) // .next(new TestCase() // - .timeleap(clock, 2, ChronoUnit.SECONDS) // - .input(BATTERY_MIN_CELL_VOLTAGE, 3050) // + .timeleap(clock, 2, SECONDS) // + .input(MIN_CELL_VOLTAGE, 3050) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 2) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 2)) // + .output(CHARGE_MAX_CURRENT, 2) // + .output(DISCHARGE_MAX_CURRENT, 2)) // .next(new TestCase() // - .timeleap(clock, 10, ChronoUnit.SECONDS) // + .timeleap(clock, 10, SECONDS) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 7) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 7)) // + .output(CHARGE_MAX_CURRENT, 7) // + .output(DISCHARGE_MAX_CURRENT, 7)) // .next(new TestCase() // - .timeleap(clock, 10, ChronoUnit.MINUTES) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3300) // + .timeleap(clock, 10, MINUTES) // + .input(MAX_CELL_VOLTAGE, 3300) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 80) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 80) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase() // - .timeleap(clock, 10, ChronoUnit.MINUTES) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3499) // + .timeleap(clock, 10, MINUTES) // + .input(MAX_CELL_VOLTAGE, 3499) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 54) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 54) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase() // - .timeleap(clock, 10, ChronoUnit.MINUTES) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3649) // + .timeleap(clock, 10, MINUTES) // + .input(MAX_CELL_VOLTAGE, 3649) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 2) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 2) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase() // - .timeleap(clock, 10, ChronoUnit.MINUTES) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3649) // + .timeleap(clock, 10, MINUTES) // + .input(MAX_CELL_VOLTAGE, 3649) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 2) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 2) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase() // - .timeleap(clock, 10, ChronoUnit.MINUTES) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3650) // + .timeleap(clock, 10, MINUTES) // + .input(MAX_CELL_VOLTAGE, 3650) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Start Force-Discharge: wait 60 seconds") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3660) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3660) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Start Force-Discharge") // - .timeleap(clock, 60, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3660) // + .timeleap(clock, 60, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3660) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, -2) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, -2) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Force-Discharge") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3640) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3640) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, -2) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, -2) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Block Charge #1") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3639) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3639) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, -1) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, -1) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Block Charge #1 still reduce by 1") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3638) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3638) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, -1) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, -1) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Block Charge #1 still reduce by 1") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3610) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3610) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Block Charge #2") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3600) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3600) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Start Force-Discharge again") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3660) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3660) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, -2) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, -2) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Force-Discharge") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3640) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3640) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, -2) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, -2) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Block Charge #1") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3639) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3639) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, -1) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, -1) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Block Charge #1 still reduce by 1") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3638) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3638) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, -1) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, -1) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Block Charge #1 still reduce by 1") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3637) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3637) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Block Charge #2") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3600) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3600) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Block Charge #3") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3450) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3450) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Finish Force-Discharge") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3449) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3449) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase() // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3400) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3400) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 0) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 0) // + .output(DISCHARGE_MAX_CURRENT, 80)) // .next(new TestCase("Allow Charge") // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(BATTERY_MAX_CELL_VOLTAGE, 3350) // + .timeleap(clock, 1, SECONDS) // + .input(MAX_CELL_VOLTAGE, 3350) // .onAfterProcessImage(() -> sut.apply()) // - .output(BATTERY_CHARGE_MAX_CURRENT, 1) // - .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .output(CHARGE_MAX_CURRENT, 1) // + .output(DISCHARGE_MAX_CURRENT, 80)) // ; } diff --git a/io.openems.edge.battery.bmw/test/io/openems/edge/battery/bmw/BmwBatteryImplTest.java b/io.openems.edge.battery.bmw/test/io/openems/edge/battery/bmw/BmwBatteryImplTest.java index fd5aa8847db..8ded6b2514c 100644 --- a/io.openems.edge.battery.bmw/test/io/openems/edge/battery/bmw/BmwBatteryImplTest.java +++ b/io.openems.edge.battery.bmw/test/io/openems/edge/battery/bmw/BmwBatteryImplTest.java @@ -1,33 +1,33 @@ package io.openems.edge.battery.bmw; +import static io.openems.edge.battery.bmw.enums.BatteryState.DEFAULT; + import org.junit.Test; -import io.openems.edge.battery.bmw.enums.BatteryState; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; public class BmwBatteryImplTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new BmwBatteryImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // - .setBatteryState(BatteryState.DEFAULT) // + .setId("battery0") // + .setModbusId("modbus0") // + .setBatteryState(DEFAULT) // .setErrorDelay(0) // .setMaxStartAttempts(0) // .setMaxStartTime(0) // .setPendingTolerance(0) // .setStartUnsuccessfulDelay(0) // .build()) // - ; + .next(new TestCase()) // + .deactivate(); } } diff --git a/io.openems.edge.battery.bydcommercial/src/io/openems/edge/battery/bydcommercial/BydBatteryBoxCommercialC130.java b/io.openems.edge.battery.bydcommercial/src/io/openems/edge/battery/bydcommercial/BydBatteryBoxCommercialC130.java index 2dedfc9f26a..72d31daa444 100644 --- a/io.openems.edge.battery.bydcommercial/src/io/openems/edge/battery/bydcommercial/BydBatteryBoxCommercialC130.java +++ b/io.openems.edge.battery.bydcommercial/src/io/openems/edge/battery/bydcommercial/BydBatteryBoxCommercialC130.java @@ -787,27 +787,27 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { LEVEL1_CHARGE_CURRENT_HIGH(Doc.of(Level.WARNING) // .text("Cluster 2 Charge Current High Alarm Level 2")), // - LEVEL2_CELL_VOLTAGE_HIGH(Doc.of(Level.FAULT) // + LEVEL2_CELL_VOLTAGE_HIGH(Doc.of(Level.WARNING) // .text("Cluster 3 Cell Voltage High Alarm Level 3")), // - LEVEL2_CELL_VOLTAGE_LOW(Doc.of(Level.FAULT) // + LEVEL2_CELL_VOLTAGE_LOW(Doc.of(Level.WARNING) // .text("Cluster 3 Cell Voltage Low Alarm Level 3")), // - LEVEL2_CELL_VOLTAGE_DIFF_TOO_BIG(Doc.of(Level.FAULT) // + LEVEL2_CELL_VOLTAGE_DIFF_TOO_BIG(Doc.of(Level.WARNING) // .text("Alarm Level 3 Battery Cells Unbalanced")), // - LEVEL2_DISCHARGE_TEMP_HIGH(Doc.of(Level.FAULT) // + LEVEL2_DISCHARGE_TEMP_HIGH(Doc.of(Level.WARNING) // .text("Cluster 3 Cell Discharge Temperature High Alarm Level 3")), // - LEVEL2_DISCHARGE_TEMP_LOW(Doc.of(Level.FAULT) // + LEVEL2_DISCHARGE_TEMP_LOW(Doc.of(Level.WARNING) // .text("Cluster 3 Cell Discharge Temperature Low Alarm Level 3")), // - LEVEL2_CHARGE_TEMP_HIGH(Doc.of(Level.FAULT) // + LEVEL2_CHARGE_TEMP_HIGH(Doc.of(Level.WARNING) // .text("Cluster 3 Cell Charge Temperature High Alarm Level 3")), // - LEVEL2_CHARGE_TEMP_LOW(Doc.of(Level.FAULT) // + LEVEL2_CHARGE_TEMP_LOW(Doc.of(Level.WARNING) // .text("Cluster 3 Cell Charge Temperature Low Alarm Level 3")), // - LEVEL2_TEMP_DIFF_TOO_BIG(Doc.of(Level.FAULT) // + LEVEL2_TEMP_DIFF_TOO_BIG(Doc.of(Level.WARNING) // .text("Cluster 3 Cell Temperature Diff High Alarm Level 3")), // - LEVEL2_POWER_POLE_HIGH(Doc.of(Level.FAULT) // + LEVEL2_POWER_POLE_HIGH(Doc.of(Level.WARNING) // .text("Cluster 3 Cell Temperature High Alarm Level 3")), // - LEVEL2_DISCHARGE_CURRENT_HIGH(Doc.of(Level.FAULT) // + LEVEL2_DISCHARGE_CURRENT_HIGH(Doc.of(Level.WARNING) // .text("Cluster 3 Discharge Current High Alarm Level 3")), // - LEVEL2_CHARGE_CURRENT_HIGH(Doc.of(Level.FAULT) // + LEVEL2_CHARGE_CURRENT_HIGH(Doc.of(Level.WARNING) // .text("Cluster 3 Charge Current High Alarm Level 3")), // ALARM_LEVEL_1_TOTAL_VOLTAGE_DIFF_HIGH(Doc.of(Level.WARNING) // @@ -822,69 +822,69 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .text("Cluster 1 Total Voltage Low Alarm Level 1")), // ALARM_LEVEL_1_TOTAL_VOLTAGE_HIGH(Doc.of(Level.WARNING) // .text("Cluster 1 Total Voltage High Alarm Level 1")), // - ALARM_FUSE(Doc.of(Level.FAULT) // + ALARM_FUSE(Doc.of(Level.WARNING) // .text(" Fuse Alarm")), // SHIELDED_SWITCH_STATE(Doc.of(Level.WARNING) // .text("Shielded switch state")), // ALARM_BAU_COMMUNICATION(Doc.of(Level.WARNING) // .text("BAU Communication Alarm")), // - ALARM_INSULATION_CHECK(Doc.of(Level.FAULT) // + ALARM_INSULATION_CHECK(Doc.of(Level.WARNING) // .text("Inuslation Resistance Alarm")), // ALARM_CURRENT_SENSOR(Doc.of(Level.WARNING) // .text("Current Sensor Alarm")), // ALARM_BCU_BMU_COMMUNICATION(Doc.of(Level.WARNING) // .text("BCU BMU Communication Alarm")), // - ALARM_CONTACTOR_ADHESION(Doc.of(Level.FAULT)// + ALARM_CONTACTOR_ADHESION(Doc.of(Level.WARNING)// .text("Contactor Adhesion Alarm ")), // ALARM_BCU_NTC(Doc.of(Level.WARNING) // .text("BCU NTC Alarm")), // ALARM_SLAVE_CONTROL_SUMMARY(Doc.of(Level.WARNING) // .text("Slave Control Summary Alarm")), // - FAILURE_INITIALIZATION(Doc.of(Level.FAULT) // + FAILURE_INITIALIZATION(Doc.of(Level.WARNING) // .text("Initialization failure")), // - FAILURE_EEPROM(Doc.of(Level.FAULT) // + FAILURE_EEPROM(Doc.of(Level.WARNING) // .text("EEPROM fault")), // - FAILURE_EEPROM2(Doc.of(Level.FAULT) // + FAILURE_EEPROM2(Doc.of(Level.WARNING) // .text("EEPROM2 fault")), // - FAILURE_INTRANET_COMMUNICATION(Doc.of(Level.FAULT) // + FAILURE_INTRANET_COMMUNICATION(Doc.of(Level.WARNING) // .text("Intranet communication fault")), // - FAILURE_TEMP_SAMPLING_LINE(Doc.of(Level.FAULT) // + FAILURE_TEMP_SAMPLING_LINE(Doc.of(Level.WARNING) // .text("Temperature sampling line fault")), // - FAILURE_BALANCING_MODULE(Doc.of(Level.FAULT) // + FAILURE_BALANCING_MODULE(Doc.of(Level.WARNING) // .text("Balancing module fault")), // - FAILURE_TEMP_SENSOR(Doc.of(Level.FAULT) // + FAILURE_TEMP_SENSOR(Doc.of(Level.WARNING) // .text("Temperature sensor fault")), // - FAILURE_TEMP_SAMPLING(Doc.of(Level.FAULT) // + FAILURE_TEMP_SAMPLING(Doc.of(Level.WARNING) // .text("Temperature sampling fault")), // - FAILURE_VOLTAGE_SAMPLING(Doc.of(Level.FAULT) // + FAILURE_VOLTAGE_SAMPLING(Doc.of(Level.WARNING) // .text("Voltage sampling fault")), // - FAILURE_VOLTAGE_SAMPLING_LINE(Doc.of(Level.FAULT) // + FAILURE_VOLTAGE_SAMPLING_LINE(Doc.of(Level.WARNING) // .text("Voltage sampling Line fault")), // - FAILURE_SLAVE_UNIT_INITIALIZATION(Doc.of(Level.FAULT) // + FAILURE_SLAVE_UNIT_INITIALIZATION(Doc.of(Level.WARNING) // .text("Failure Slave Unit Initialization")), - FAILURE_CONNECTING_LINE(Doc.of(Level.FAULT) // + FAILURE_CONNECTING_LINE(Doc.of(Level.WARNING) // .text("Connecting Line Failure")), // - FAILURE_SAMPLING_CHIP(Doc.of(Level.FAULT) // + FAILURE_SAMPLING_CHIP(Doc.of(Level.WARNING) // .text("Sampling Chip Failure")), // - FAILURE_CONTACTOR(Doc.of(Level.FAULT) // + FAILURE_CONTACTOR(Doc.of(Level.WARNING) // .text("Contactor Failure")), // - FAILURE_PASSIVE_BALANCE(Doc.of(Level.FAULT) // + FAILURE_PASSIVE_BALANCE(Doc.of(Level.WARNING) // .text("Passive Balance Failure")), // - FAILURE_PASSIVE_BALANCE_TEMP(Doc.of(Level.FAULT) // + FAILURE_PASSIVE_BALANCE_TEMP(Doc.of(Level.WARNING) // .text("Passive Balance Temp Failure")), // - FAILURE_ACTIVE_BALANCE(Doc.of(Level.FAULT) // + FAILURE_ACTIVE_BALANCE(Doc.of(Level.WARNING) // .text("Active Balance Failure")), // - FAILURE_LTC6803(Doc.of(Level.FAULT) // + FAILURE_LTC6803(Doc.of(Level.WARNING) // .text("LTC6803 sfault")), // - FAILURE_CONNECTOR_WIRE(Doc.of(Level.FAULT) // + FAILURE_CONNECTOR_WIRE(Doc.of(Level.WARNING) // .text("connector wire fault")), // - FAILURE_SAMPLING_WIRE(Doc.of(Level.FAULT) // + FAILURE_SAMPLING_WIRE(Doc.of(Level.WARNING) // .text("sampling wire fault")), // - PRECHARGE_TAKING_TOO_LONG(Doc.of(Level.FAULT) // + PRECHARGE_TAKING_TOO_LONG(Doc.of(Level.WARNING) // .text("precharge time was too long")), // NEED_CHARGE(Doc.of(Level.WARNING) // .text("Battery Need Charge")), // - FAULT(Doc.of(Level.FAULT) // + FAULT(Doc.of(Level.WARNING) // .text("battery fault state")), // STATE_MACHINE(Doc.of(State.values()) // .text("Current State of State-Machine")), // @@ -928,76 +928,76 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .text("ALARM LEVEL 2 SOH LOWER")), // LEVEL1_PACK_TEMP_HIGH(Doc.of(Level.WARNING) // .text("ALARM LEVEL 2 PACK TEMP HIGH")), // - LEVEL2_SYSTEM_VOLTAGE_HIGH(Doc.of(Level.FAULT) // + LEVEL2_SYSTEM_VOLTAGE_HIGH(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 SYSTEM VOLTAGE HIGH")), // - LEVEL2_SYSTEM_VOLTAGE_LOW(Doc.of(Level.FAULT) // + LEVEL2_SYSTEM_VOLTAGE_LOW(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 SYSTEM VOLTAGE LOW")), // - LEVEL2_SYSTEM_VOLTAGE_UNBALANCED(Doc.of(Level.FAULT) // + LEVEL2_SYSTEM_VOLTAGE_UNBALANCED(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 SYSTEM VOLTAGE UNBALANCED")), // - LEVEL2_INSULATION_RESISTANCE_LOWER(Doc.of(Level.FAULT) // + LEVEL2_INSULATION_RESISTANCE_LOWER(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 INSULATION RESISTANCE LOWER")), // - LEVEL2_POS_INSULATION_RESISTANCE_LOWER(Doc.of(Level.FAULT) // + LEVEL2_POS_INSULATION_RESISTANCE_LOWER(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 POS INSULATION RESISTANCE LOWER")), // - LEVEL2_NEG_INSULATION_RESISTANCE_LOWER(Doc.of(Level.FAULT) // + LEVEL2_NEG_INSULATION_RESISTANCE_LOWER(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 NEG INSULATION RESISTANCE LOWER")), // - LEVEL2_SYSTEM_SOC_LOWER(Doc.of(Level.FAULT) // + LEVEL2_SYSTEM_SOC_LOWER(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 SYSTEM SOC LOWER")), // - LEVEL2_SYSTEM_SOC_HIGH(Doc.of(Level.FAULT) // + LEVEL2_SYSTEM_SOC_HIGH(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 SYSTEM SOC HIGH")), // LEVEL2_SOH_LOWER(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 SOH LOWER")), // - LEVEL2_PACK_TEMP_HIGH(Doc.of(Level.FAULT) // + LEVEL2_PACK_TEMP_HIGH(Doc.of(Level.WARNING) // .text("ALARM LEVEL 3 PACK TEMP HIGH")), // - SLAVE_11_COMMUNICATION_ERROR(Doc.of(Level.FAULT)// + SLAVE_11_COMMUNICATION_ERROR(Doc.of(Level.WARNING)// .text("Master control and Slave control Communication Fault 1 SLAVE_CTRL_11")), // - SLAVE_12_COMMUNICATION_ERROR(Doc.of(Level.FAULT)// + SLAVE_12_COMMUNICATION_ERROR(Doc.of(Level.WARNING)// .text("Master control and Slave control Communication Fault 1 SLAVE_CTRL_12")), // - SLAVE_13_COMMUNICATION_ERROR(Doc.of(Level.FAULT)// + SLAVE_13_COMMUNICATION_ERROR(Doc.of(Level.WARNING)// .text("Master control and Slave control Communication Fault 1 SLAVE_CTRL_13")), // - SLAVE_14_COMMUNICATION_ERROR(Doc.of(Level.FAULT)// + SLAVE_14_COMMUNICATION_ERROR(Doc.of(Level.WARNING)// .text("Master control and Slave control Communication Fault 1 SLAVE_CTRL_14")), // - SLAVE_15_COMMUNICATION_ERROR(Doc.of(Level.FAULT)// + SLAVE_15_COMMUNICATION_ERROR(Doc.of(Level.WARNING)// .text("Master control and Slave control Communication Fault 1 SLAVE_CTRL_15")), // - SLAVE_16_COMMUNICATION_ERROR(Doc.of(Level.FAULT)// + SLAVE_16_COMMUNICATION_ERROR(Doc.of(Level.WARNING)// .text("Master control and Slave control Communication Fault 1 SLAVE_CTRL_16")), // - SLAVE_17_COMMUNICATION_ERROR(Doc.of(Level.FAULT)// + SLAVE_17_COMMUNICATION_ERROR(Doc.of(Level.WARNING)// .text("Master control and Slave control Communication Fault 1 SLAVE_CTRL_17")), // - SLAVE_18_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_18_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_18")), // - SLAVE_19_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_19_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_19")), // - SLAVE_20_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_20_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_20")), // - SLAVE_21_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_21_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_21")), // - SLAVE_22_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_22_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_22")), // - SLAVE_23_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_23_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_23")), // - SLAVE_24_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_24_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_24")), // - SLAVE_25_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_25_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_25")), // - SLAVE_26_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_26_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_26")), // - SLAVE_27_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_27_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_27")), // - SLAVE_28_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_28_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_28")), // - SLAVE_29_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_29_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_29")), // - SLAVE_30_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_30_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_30")), // - SLAVE_31_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_31_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_31")), // - SLAVE_32_COMMUNICATION_ERROR(Doc.of(Level.FAULT) // + SLAVE_32_COMMUNICATION_ERROR(Doc.of(Level.WARNING) // .text("Master control and Slave control Communication Fault 2 SLAVE_CTRL_31")), // // OpenEMS Faults - RUN_FAILED(Doc.of(Level.FAULT) // + RUN_FAILED(Doc.of(Level.WARNING) // .text("Running the Logic failed")), // - MAX_START_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_START_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of start attempts failed")), // - MAX_STOP_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_STOP_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of stop attempts failed")), // ; diff --git a/io.openems.edge.battery.bydcommercial/test/io/openems/edge/battery/bydcommercial/BydBatteryBoxCommercialC130ImplTest.java b/io.openems.edge.battery.bydcommercial/test/io/openems/edge/battery/bydcommercial/BydBatteryBoxCommercialC130ImplTest.java index 91d0b85e1c1..1b87571c888 100644 --- a/io.openems.edge.battery.bydcommercial/test/io/openems/edge/battery/bydcommercial/BydBatteryBoxCommercialC130ImplTest.java +++ b/io.openems.edge.battery.bydcommercial/test/io/openems/edge/battery/bydcommercial/BydBatteryBoxCommercialC130ImplTest.java @@ -1,28 +1,30 @@ package io.openems.edge.battery.bydcommercial; +import static io.openems.edge.common.startstop.StartStopConfig.AUTO; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; -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; public class BydBatteryBoxCommercialC130ImplTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new BydBatteryBoxCommercialC130Impl()) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // - .setStartStop(StartStopConfig.AUTO) // + .setId("battery0") // + .setModbusId("modbus0") // + .setStartStop(AUTO) // .build()) // - ; + .next(new TestCase()) // + .deactivate(); } } diff --git a/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/BatteryFeneconCommercialImplTest.java b/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/BatteryFeneconCommercialImplTest.java index 1226ccc9ffc..8fec9839f16 100644 --- a/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/BatteryFeneconCommercialImplTest.java +++ b/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/BatteryFeneconCommercialImplTest.java @@ -1,17 +1,20 @@ package io.openems.edge.battery.fenecon.commercial; -import java.time.Instant; -import java.time.ZoneOffset; +import static io.openems.edge.battery.api.Battery.ChannelId.CHARGE_MAX_CURRENT; +import static io.openems.edge.battery.api.Battery.ChannelId.DISCHARGE_MAX_CURRENT; +import static io.openems.edge.battery.api.Battery.ChannelId.SOC; +import static io.openems.edge.battery.fenecon.commercial.BatteryFeneconCommercial.ChannelId.BATTERY_SOC; +import static io.openems.edge.battery.fenecon.commercial.BatteryFeneconCommercial.ChannelId.RUNNING; +import static io.openems.edge.battery.fenecon.commercial.BatteryFeneconCommercial.ChannelId.STATE_MACHINE; +import static io.openems.edge.common.startstop.StartStoppable.ChannelId.START_STOP; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT7; import org.junit.Test; -import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; -import io.openems.edge.battery.api.Battery; import io.openems.edge.battery.fenecon.commercial.statemachine.StateMachine; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.startstop.StartStopConfig; -import io.openems.edge.common.startstop.StartStoppable; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; @@ -20,42 +23,23 @@ public class BatteryFeneconCommercialImplTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - private static final String IO_ID = "io0"; - - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(BATTERY_ID, - BatteryFeneconCommercial.ChannelId.STATE_MACHINE.id()); - private static final ChannelAddress RUNNING = new ChannelAddress(BATTERY_ID, - BatteryFeneconCommercial.ChannelId.RUNNING.id()); - private static final ChannelAddress BATTERY_RELAY = new ChannelAddress(IO_ID, "InputOutput7"); - private static final ChannelAddress START_STOP = new ChannelAddress(BATTERY_ID, - StartStoppable.ChannelId.START_STOP.id()); - private static final ChannelAddress BATTERY_SOC = new ChannelAddress(BATTERY_ID, - BatteryFeneconCommercial.ChannelId.BATTERY_SOC.id()); - private static final ChannelAddress BATTERY_MAX_DISCHARGE_CURRENT = new ChannelAddress(BATTERY_ID, - Battery.ChannelId.DISCHARGE_MAX_CURRENT.id()); - private static final ChannelAddress BATTERY_MAX_CHARGE_CURRENT = new ChannelAddress(BATTERY_ID, - Battery.ChannelId.CHARGE_MAX_CURRENT.id()); - private static final ChannelAddress SOC = new ChannelAddress(BATTERY_ID, Battery.ChannelId.SOC.id()); - @Test public void startBattery() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); new ComponentTest(new BatteryFeneconCommercialImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID))// + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0"))// .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(1) // .setStartStop(StartStopConfig.START) // .setBatteryStartStopRelay("io0/InputOutput7")// .build())// .next(new TestCase("Battery Relay false, starting") // - .input(BATTERY_RELAY, false)// + .input("io0", INPUT_OUTPUT7, false)// .input(RUNNING, false)// Switched Off .output(STATE_MACHINE, StateMachine.State.UNDEFINED))// .next(new TestCase() // @@ -63,18 +47,18 @@ public void startBattery() throws Exception { .next(new TestCase()// .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// .next(new TestCase()// - .input(BATTERY_RELAY, true))// + .input("io0", INPUT_OUTPUT7, true))// .next(new TestCase() // .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// .next(new TestCase()// .input(RUNNING, true)// - .input(BATTERY_RELAY, false))// + .input("io0", INPUT_OUTPUT7, false))// .next(new TestCase() // .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// .next(new TestCase("Battery Running")// .output(STATE_MACHINE, StateMachine.State.RUNNING))// .next(new TestCase("Battery Running")// - .output(BATTERY_RELAY, false)// + .output("io0", INPUT_OUTPUT7, false)// .output(RUNNING, true) // .output(STATE_MACHINE, StateMachine.State.RUNNING))// @@ -83,21 +67,21 @@ public void startBattery() throws Exception { @Test public void stopBattery() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); new ComponentTest(new BatteryFeneconCommercialImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID))// + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0"))// .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(1) // .setStartStop(StartStopConfig.STOP) // .setBatteryStartStopRelay("io0/InputOutput7")// .build())// .next(new TestCase("Battery Running")// - .input(BATTERY_RELAY, false)// + .input("io0", INPUT_OUTPUT7, false)// .input(RUNNING, true) // .input(STATE_MACHINE, StateMachine.State.RUNNING))// .next(new TestCase("Stopping") // @@ -105,7 +89,7 @@ public void stopBattery() throws Exception { .next(new TestCase()// .output(STATE_MACHINE, StateMachine.State.GO_STOPPED))// .next(new TestCase()// - .input(BATTERY_RELAY, true)) // + .input("io0", INPUT_OUTPUT7, true)) // .next(new TestCase()// .output(STATE_MACHINE, StateMachine.State.STOPPED))// @@ -114,15 +98,15 @@ public void stopBattery() throws Exception { @Test public void socManipulationMin() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); new ComponentTest(new BatteryFeneconCommercialImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID))// + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0"))// .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(1) // .setStartStop(StartStopConfig.START) // .setBatteryStartStopRelay("io0/InputOutput7")// @@ -130,22 +114,22 @@ public void socManipulationMin() throws Exception { .next(new TestCase("Soc")// .input(RUNNING, true) // .input(BATTERY_SOC, 10) // - .input(BATTERY_MAX_DISCHARGE_CURRENT, 0) // - .input(BATTERY_MAX_CHARGE_CURRENT, 10000) // + .input(DISCHARGE_MAX_CURRENT, 0) // + .input(CHARGE_MAX_CURRENT, 10000) // .output(SOC, 10)); } @Test public void socManipulationMax() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); new ComponentTest(new BatteryFeneconCommercialImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID))// + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0"))// .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(1) // .setStartStop(StartStopConfig.START) // .setBatteryStartStopRelay("io0/InputOutput7")// @@ -153,8 +137,8 @@ public void socManipulationMax() throws Exception { .next(new TestCase("Soc")// .input(RUNNING, true) // .input(BATTERY_SOC, 98) // - .input(BATTERY_MAX_DISCHARGE_CURRENT, 100000) // - .input(BATTERY_MAX_CHARGE_CURRENT, 0) // + .input(DISCHARGE_MAX_CURRENT, 100000) // + .input(CHARGE_MAX_CURRENT, 0) // .output(SOC, 100)); } } diff --git a/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/DynamicChannelsAndSerialNumbersTest.java b/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/DynamicChannelsAndSerialNumbersTest.java index b28b1513542..a9db6249dc3 100644 --- a/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/DynamicChannelsAndSerialNumbersTest.java +++ b/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/DynamicChannelsAndSerialNumbersTest.java @@ -1,10 +1,14 @@ package io.openems.edge.battery.fenecon.commercial; +import static io.openems.edge.battery.fenecon.commercial.BatteryFeneconCommercial.ChannelId.MASTER_MCU_HARDWARE_VERSION; +import static io.openems.edge.battery.fenecon.commercial.BatteryFeneconCommercial.ChannelId.NUMBER_OF_CELLS_PER_MODULE; +import static io.openems.edge.battery.fenecon.commercial.BatteryFeneconCommercial.ChannelId.NUMBER_OF_MODULES_PER_TOWER; +import static io.openems.edge.battery.fenecon.commercial.BatteryFeneconCommercial.ChannelId.NUMBER_OF_TOWERS; +import static io.openems.edge.battery.fenecon.commercial.BatteryFeneconCommercialImpl.VERSION_CONVERTER; import static org.junit.Assert.assertEquals; import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.api.Battery; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.startstop.StartStopConfig; @@ -16,24 +20,10 @@ public class DynamicChannelsAndSerialNumbersTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - private static final String IO_ID = "io0"; - private static final int TOWERS = 1; private static final int MODULES = 10; private static final int CELLS = 120;/* Read from register as cells*modules */ - private static final ChannelAddress NUMBER_OF_MODULES_PER_TOWER = new ChannelAddress(BATTERY_ID, - "NumberOfModulesPerTower"); - private static final ChannelAddress NUMBER_OF_TOWERS = new ChannelAddress(BATTERY_ID, "NumberOfTowers"); - private static final ChannelAddress NUMBER_OF_CELLS_PER_MODULE = new ChannelAddress(BATTERY_ID, - "NumberOfCellsPerModule"); - private static final ChannelAddress SUB_MASTER_HARDWARE_VERSION = new ChannelAddress(BATTERY_ID, - "Tower0SubMasterHardwareVersion"); - private static final ChannelAddress MASTER_MCU_HARDWARE_VERSION = new ChannelAddress(BATTERY_ID, - "MasterMcuHardwareVersion"); - @Test public void testSerialNum() throws Exception { var battery = new BatteryFeneconCommercialImpl(); @@ -41,11 +31,11 @@ public void testSerialNum() throws Exception { var componentTest = new ComponentTest(battery) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID))// + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0"))// .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setBatteryStartStopRelay("io0/InputOutput0")// .setStartStop(StartStopConfig.AUTO) // @@ -56,10 +46,10 @@ public void testSerialNum() throws Exception { .input(NUMBER_OF_TOWERS, TOWERS) // .input(NUMBER_OF_MODULES_PER_TOWER, MODULES) // .input(NUMBER_OF_CELLS_PER_MODULE, CELLS) // - .input(SUB_MASTER_HARDWARE_VERSION, "109101BM60")); + .input("battery0", "Tower0SubMasterHardwareVersion", "109101BM60")); checkDynamicChannels(battery, TOWERS, MODULES, CELLS / MODULES); - assertEquals("011910MB06", BatteryFeneconCommercialImpl.VERSION_CONVERTER.elementToChannel("109101BM60")); + assertEquals("011910MB06", VERSION_CONVERTER.elementToChannel("109101BM60")); componentTest.next(new TestCase()); componentTest.next(new TestCase()); @@ -72,7 +62,7 @@ public void testSerialNum() throws Exception { .input(NUMBER_OF_CELLS_PER_MODULE, CELLS) // .input(MASTER_MCU_HARDWARE_VERSION, "100201MS50")); - assertEquals("012010SM05", BatteryFeneconCommercialImpl.VERSION_CONVERTER.elementToChannel("100201MS50")); + assertEquals("012010SM05", VERSION_CONVERTER.elementToChannel("100201MS50")); } /** diff --git a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHome.java b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHome.java index 0ea7cc798ae..d4298dde150 100644 --- a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHome.java +++ b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHome.java @@ -673,7 +673,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId STATE_MACHINE(Doc.of(State.values()) // .text("Current State of State-Machine")), // - RUN_FAILED(Doc.of(Level.FAULT) // + RUN_FAILED(Doc.of(Level.WARNING) // .text("Running the Logic failed")), // LOW_MIN_VOLTAGE_WARNING(Doc.of(Level.WARNING) // .text("Low min voltage warning " diff --git a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImpl.java b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImpl.java index efa68f7d43d..e9d933bc5e0 100644 --- a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImpl.java +++ b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImpl.java @@ -117,17 +117,12 @@ public BatteryFeneconHomeImpl() { BatteryProtection.ChannelId.values(), // BatteryFeneconHome.ChannelId.values() // ); - this.updateHardwareType(BatteryFeneconHomeHardwareType.DEFAULT); } @Activate private void activate(ComponentContext context, Config config) throws OpenemsException { this.config = config; - - // Predefine BatteryProtection. Later adapted to the hardware type. - this.batteryProtection = BatteryProtection.create(this) // - .applyBatteryProtectionDefinition(new FeneconHomeBatteryProtection52(), this.componentManager) // - .build(); + this.updateHardwareType(BatteryFeneconHomeHardwareType.DEFAULT); // initialize to default if (super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm, "Modbus", config.modbus_id())) { @@ -194,7 +189,7 @@ private void handleStateMachine() { @Override protected ModbusProtocol defineModbusProtocol() { return new ModbusProtocol(this, // - new FC3ReadRegistersTask(500, Priority.LOW, // + new FC3ReadRegistersTask(500, Priority.HIGH, // m(new BitsWordElement(500, this) // .bit(0, BatteryFeneconHome.ChannelId.RACK_PRE_ALARM_CELL_OVER_VOLTAGE) // .bit(1, BatteryFeneconHome.ChannelId.RACK_PRE_ALARM_CELL_UNDER_VOLTAGE) // @@ -271,10 +266,7 @@ protected ModbusProtocol defineModbusProtocol() { .bit(6, BatteryFeneconHome.ChannelId.FAULT_POSITION_BCU_7) // .bit(7, BatteryFeneconHome.ChannelId.FAULT_POSITION_BCU_8) // .bit(8, BatteryFeneconHome.ChannelId.FAULT_POSITION_BCU_9) // - .bit(9, BatteryFeneconHome.ChannelId.FAULT_POSITION_BCU_10))// - ), // - - new FC3ReadRegistersTask(506, Priority.LOW, // + .bit(9, BatteryFeneconHome.ChannelId.FAULT_POSITION_BCU_10)), // m(Battery.ChannelId.VOLTAGE, new UnsignedWordElement(506), SCALE_FACTOR_MINUS_1), // [V] m(Battery.ChannelId.CURRENT, new SignedWordElement(507), SCALE_FACTOR_MINUS_1), // [A] m(Battery.ChannelId.SOC, new UnsignedWordElement(508), SCALE_FACTOR_MINUS_1), // [%] @@ -1066,7 +1058,7 @@ public BridgeModbus getModbus() { } @Override - public ModbusProtocol getDefinedModbusProtocol() throws OpenemsException { + public ModbusProtocol getDefinedModbusProtocol() { return this.getModbusProtocol(); } diff --git a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/FeneconHomeBatteryProtection64.java b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/FeneconHomeBatteryProtection64.java index bbad1e67bde..65dec26fc83 100644 --- a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/FeneconHomeBatteryProtection64.java +++ b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/FeneconHomeBatteryProtection64.java @@ -25,8 +25,8 @@ public PolyLine getChargeVoltageToPercent() { .addPoint(Math.nextUp(3000), 1) // .addPoint(3450, 1) // .addPoint(3540, 0.08) // - .addPoint(Math.nextDown(3550), 0.08) // - .addPoint(3550, 0) // + .addPoint(Math.nextDown(3580), 0.08) // + .addPoint(3580, 0) // .build(); } @@ -70,7 +70,7 @@ public PolyLine getDischargeSocToPercent() { @Override public ForceDischarge.Params getForceDischargeParams() { - return new ForceDischarge.Params(3600, 3540, 3450); + return new ForceDischarge.Params(3630, 3540, 3450); } @Override diff --git a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/ModbusHelper.java b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/ModbusHelper.java index b072d1ebdc4..4f95aa64a66 100644 --- a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/ModbusHelper.java +++ b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/ModbusHelper.java @@ -1,24 +1,22 @@ package io.openems.edge.battery.fenecon.home; -import io.openems.common.exceptions.OpenemsException; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusProtocol; public interface ModbusHelper { /** - * Get modbus bridge. + * Get the {@link BridgeModbus}. * - * @return modbus bridge. + * @return the {@link BridgeModbus} */ public BridgeModbus getModbus(); /** - * Get defined modbus protocol. + * Get defined {@link ModbusProtocol}. * - * @return modbus protocol - * @throws OpenemsException on error + * @return the {@link ModbusProtocol} */ - public ModbusProtocol getDefinedModbusProtocol() throws OpenemsException; + public ModbusProtocol getDefinedModbusProtocol(); } diff --git a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/statemachine/Context.java b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/statemachine/Context.java index a2828143f76..6cb49490bb6 100644 --- a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/statemachine/Context.java +++ b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/statemachine/Context.java @@ -32,8 +32,4 @@ public Context(BatteryFeneconHome parent, Clock clock, Boolean batteryStartUpRel this.modbusCommunicationFailed = modbusCommunicationFailed; this.retryModbusCommunication = retryModbusCommunication; } - - protected void retryModbusCommunication() { - this.getParent().retryModbusCommunication(); - } } \ No newline at end of file diff --git a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/statemachine/GoStoppedHandler.java b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/statemachine/GoStoppedHandler.java index a41a0bf4837..d539b27df30 100644 --- a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/statemachine/GoStoppedHandler.java +++ b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/statemachine/GoStoppedHandler.java @@ -12,26 +12,25 @@ public class GoStoppedHandler extends StateHandler { private static int TIMEOUT = 2100; // [35 minutes in seconds] private Instant timeAtEntry = Instant.MIN; - private boolean didProtocolAdd = false; + private boolean isProtocolAdded = false; @Override - protected void onEntry(Context context) throws OpenemsNamedException { - final var battery = context.getParent(); - final var modbus = battery.getModbus(); - modbus.removeProtocol(battery.id()); - this.didProtocolAdd = false; + protected void onEntry(Context context) { + // Remove the protocol to trigger BMS timeout + this.removeProtocol(context); + this.timeAtEntry = Instant.now(context.clock); } @Override public State runAndGetNextState(Context context) throws OpenemsException { - final var battery = context.getParent(); var now = Instant.now(context.clock); - if (Duration.between(this.timeAtEntry, now).getSeconds() > TIMEOUT && !this.didProtocolAdd) { - this.addAndRetryModbusProtocol(context); + if (Duration.between(this.timeAtEntry, now).getSeconds() > TIMEOUT && !this.isProtocolAdded) { + this.addProtocol(context); return State.GO_STOPPED; } + final var battery = context.getParent(); if (battery.getModbusCommunicationFailed()) { return State.STOPPED; } @@ -40,12 +39,25 @@ public State runAndGetNextState(Context context) throws OpenemsException { return State.GO_STOPPED; } - private void addAndRetryModbusProtocol(Context context) throws OpenemsException { + @Override + protected void onExit(Context context) throws OpenemsNamedException { + // Make sure to leave this GoStoppedHandler with added protocol + if (!this.isProtocolAdded) { + this.addProtocol(context); + } + } + + private void addProtocol(Context context) { final var battery = context.getParent(); final var modbus = battery.getModbus(); + this.isProtocolAdded = true; modbus.addProtocol(battery.id(), battery.getDefinedModbusProtocol()); - modbus.retryModbusCommunication(battery.id()); - this.didProtocolAdd = true; } + private void removeProtocol(Context context) { + final var battery = context.getParent(); + final var modbus = battery.getModbus(); + this.isProtocolAdded = false; + modbus.removeProtocol(battery.id()); + } } diff --git a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java index 432eccdcb6e..b0029262635 100644 --- a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java +++ b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java @@ -1,21 +1,38 @@ package io.openems.edge.battery.fenecon.home; +import static io.openems.edge.battery.api.Battery.ChannelId.CHARGE_MAX_CURRENT; +import static io.openems.edge.battery.api.Battery.ChannelId.CURRENT; +import static io.openems.edge.battery.api.Battery.ChannelId.MAX_CELL_VOLTAGE; +import static io.openems.edge.battery.api.Battery.ChannelId.MIN_CELL_VOLTAGE; +import static io.openems.edge.battery.api.Battery.ChannelId.SOC; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.BMS_CONTROL; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.LOW_MIN_VOLTAGE_FAULT; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.LOW_MIN_VOLTAGE_WARNING; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.NUMBER_OF_MODULES_PER_TOWER; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.NUMBER_OF_TOWERS; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.STATE_MACHINE; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.TOWER_0_BMS_SOFTWARE_VERSION; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.TOWER_1_BMS_SOFTWARE_VERSION; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.TOWER_2_BMS_SOFTWARE_VERSION; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.TOWER_3_BMS_SOFTWARE_VERSION; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.TOWER_4_BMS_SOFTWARE_VERSION; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHomeImpl.TIMEOUT; +import static io.openems.edge.battery.protection.BatteryProtection.ChannelId.BP_CHARGE_BMS; +import static io.openems.edge.battery.protection.BatteryProtection.ChannelId.BP_CHARGE_MAX_SOC; +import static io.openems.edge.bridge.modbus.api.ModbusComponent.ChannelId.MODBUS_COMMUNICATION_FAILED; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT4; +import static java.lang.Math.round; +import static java.time.temporal.ChronoUnit.SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; - import org.junit.Test; import io.openems.common.function.ThrowingRunnable; -import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; -import io.openems.edge.battery.api.Battery; -import io.openems.edge.battery.fenecon.home.statemachine.StateMachine; -import io.openems.edge.battery.protection.BatteryProtection; -import io.openems.edge.bridge.modbus.api.ModbusComponent; +import io.openems.edge.battery.fenecon.home.statemachine.StateMachine.State; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.startstop.StartStopConfig; import io.openems.edge.common.test.AbstractComponentTest.TestCase; @@ -26,51 +43,6 @@ public class BatteryFeneconHomeImplTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - private static final String IO_ID = "io0"; - - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.STATE_MACHINE.id()); - private static final ChannelAddress LOW_MIN_VOLTAGE_WARNING = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.LOW_MIN_VOLTAGE_WARNING.id()); - private static final ChannelAddress LOW_MIN_VOLTAGE_FAULT = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.LOW_MIN_VOLTAGE_FAULT.id()); - private static final ChannelAddress LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED.id()); - private static final ChannelAddress MODBUS_COMMUNICATION_FAILED = new ChannelAddress(BATTERY_ID, - ModbusComponent.ChannelId.MODBUS_COMMUNICATION_FAILED.id()); - private static final ChannelAddress BMS_CONTROL = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.BMS_CONTROL.id()); - private static final ChannelAddress BP_CHARGE_BMS = new ChannelAddress(BATTERY_ID, - BatteryProtection.ChannelId.BP_CHARGE_BMS.id()); - private static final ChannelAddress MAX_CELL_VOLTAGE = new ChannelAddress(BATTERY_ID, - Battery.ChannelId.MAX_CELL_VOLTAGE.id()); - private static final ChannelAddress CHARGE_MAX_CURRENT = new ChannelAddress(BATTERY_ID, - Battery.ChannelId.CHARGE_MAX_CURRENT.id()); - private static final ChannelAddress CURRENT = new ChannelAddress(BATTERY_ID, Battery.ChannelId.CURRENT.id()); - private static final ChannelAddress MIN_CELL_VOLTAGE = new ChannelAddress(BATTERY_ID, - Battery.ChannelId.MIN_CELL_VOLTAGE.id()); - private static final ChannelAddress TOWER_0_BMS_SOFTWARE_VERSION = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.TOWER_0_BMS_SOFTWARE_VERSION.id()); - private static final ChannelAddress TOWER_1_BMS_SOFTWARE_VERSION = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.TOWER_1_BMS_SOFTWARE_VERSION.id()); - private static final ChannelAddress TOWER_2_BMS_SOFTWARE_VERSION = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.TOWER_2_BMS_SOFTWARE_VERSION.id()); - private static final ChannelAddress TOWER_3_BMS_SOFTWARE_VERSION = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.TOWER_3_BMS_SOFTWARE_VERSION.id()); - private static final ChannelAddress TOWER_4_BMS_SOFTWARE_VERSION = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.TOWER_4_BMS_SOFTWARE_VERSION.id()); - private static final ChannelAddress NUMBER_OF_TOWERS = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.NUMBER_OF_TOWERS.id()); - private static final ChannelAddress NUMBER_OF_MODULES_PER_TOWER = new ChannelAddress(BATTERY_ID, - BatteryFeneconHome.ChannelId.NUMBER_OF_MODULES_PER_TOWER.id()); - private static final ChannelAddress BP_CHARGE_MAX_SOC = new ChannelAddress(BATTERY_ID, - BatteryProtection.ChannelId.BP_CHARGE_MAX_SOC.id()); - private static final ChannelAddress SOC = new ChannelAddress(BATTERY_ID, Battery.ChannelId.SOC.id()); - - private static final ChannelAddress BATTERY_RELAY = new ChannelAddress(IO_ID, "InputOutput4"); - private static ThrowingRunnable assertLog(BatteryFeneconHomeImpl sut, String message) { return () -> assertEquals(message, sut.stateMachine.debugLog()); } @@ -82,41 +54,41 @@ private static ThrowingRunnable assertLog(BatteryFeneconHomeImpl sut, */ @Test public void test() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); var sut = new BatteryFeneconHomeImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID))// + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0"))// .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setStartStop(StartStopConfig.START) // .setBatteryStartUpRelay("io0/InputOutput4")// .build())// .next(new TestCase() // .inputForce(MODBUS_COMMUNICATION_FAILED, true) // - .input(BATTERY_RELAY, false) // Switch OFF + .input("io0", INPUT_OUTPUT4, false) // Switch OFF .input(BMS_CONTROL, false) // Switched OFF .onBeforeProcessImage(assertLog(sut, "Undefined")) // - .output(STATE_MACHINE, StateMachine.State.UNDEFINED)) // + .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-Undefined")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase()// .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOn"))) // .next(new TestCase()// - .input(BATTERY_RELAY, true) // Switch ON + .input("io0", INPUT_OUTPUT4, true) // Switch ON .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOn"))) // .next(new TestCase()// - .input(BATTERY_RELAY, true) // Switch ON + .input("io0", INPUT_OUTPUT4, true) // Switch ON .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayHold")) - .onAfterProcessImage(() -> clock.leap(11, ChronoUnit.SECONDS))) // + .onAfterProcessImage(() -> clock.leap(11, SECONDS))) // .next(new TestCase() // - .input(BATTERY_RELAY, false) // Switch OFF + .input("io0", INPUT_OUTPUT4, false) // Switch OFF .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff"))) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-RetryModbusCommunication"))) // @@ -129,17 +101,17 @@ public void test() throws Exception { .next(new TestCase()// .onBeforeProcessImage(assertLog(sut, "Running")) // - .output(STATE_MACHINE, StateMachine.State.RUNNING)) // + .output(STATE_MACHINE, State.RUNNING)) // // Ramp-Up ChargeMaxCurrent (0.1 A / Second) .next(new TestCase() // .input(BP_CHARGE_BMS, 40) // .input(MAX_CELL_VOLTAGE, 3000)) // .next(new TestCase("Ramp up") // - .timeleap(clock, 100, ChronoUnit.SECONDS) // + .timeleap(clock, 100, SECONDS) // .output(CHARGE_MAX_CURRENT, 10)) // .next(new TestCase() // - .timeleap(clock, 300, ChronoUnit.SECONDS) // + .timeleap(clock, 300, SECONDS) // .output(CHARGE_MAX_CURRENT, 40)) // Full Battery @@ -151,7 +123,7 @@ public void test() throws Exception { .next(new TestCase() // .input(BP_CHARGE_BMS, 40)) // .next(new TestCase() // - .timeleap(clock, 100, ChronoUnit.SECONDS) // + .timeleap(clock, 100, SECONDS) // .output(CHARGE_MAX_CURRENT, 25)) // ; } @@ -163,54 +135,54 @@ public void test() throws Exception { */ @Test public void test2() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); var sut = new BatteryFeneconHomeImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setStartStop(StartStopConfig.START) // .setBatteryStartUpRelay("io0/InputOutput4")// .build())// .next(new TestCase()// - .input(BATTERY_RELAY, true) // + .input("io0", INPUT_OUTPUT4, true) // .input(BMS_CONTROL, false) // Switched Off - .output(STATE_MACHINE, StateMachine.State.UNDEFINED))// + .output(STATE_MACHINE, State.UNDEFINED))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-Undefined")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOn")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase()// - .input(BATTERY_RELAY, true) // Switch ON + .input("io0", INPUT_OUTPUT4, true) // Switch ON .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayHold")) - .onAfterProcessImage(() -> clock.leap(11, ChronoUnit.SECONDS))) // + .onAfterProcessImage(() -> clock.leap(11, SECONDS))) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff")) // .input(BMS_CONTROL, true) // Switched On - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff")) // - .input(BATTERY_RELAY, false) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .input("io0", INPUT_OUTPUT4, false) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-RetryModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForBmsControl")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // - .output(STATE_MACHINE, StateMachine.State.RUNNING)); + .output(STATE_MACHINE, State.RUNNING)); } /** @@ -225,37 +197,37 @@ public void test3() throws Exception { new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID))// + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0"))// .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setStartStop(StartStopConfig.START) // .setBatteryStartUpRelay("io0/InputOutput4")// .build()) // .next(new TestCase() // - .input(BATTERY_RELAY, false) // + .input("io0", INPUT_OUTPUT4, false) // .input(BMS_CONTROL, true) // Switched On - .output(STATE_MACHINE, StateMachine.State.UNDEFINED))// + .output(STATE_MACHINE, State.UNDEFINED))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-Undefined")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-RetryModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForBmsControl")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // - .output(STATE_MACHINE, StateMachine.State.RUNNING)); + .output(STATE_MACHINE, State.RUNNING)); } /** @@ -265,53 +237,53 @@ public void test3() throws Exception { */ @Test public void test4() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); var sut = new BatteryFeneconHomeImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setStartStop(StartStopConfig.START) // .setBatteryStartUpRelay("io0/InputOutput4") // .build()) // .next(new TestCase() // - .input(BATTERY_RELAY, false) // + .input("io0", INPUT_OUTPUT4, false) // .input(BMS_CONTROL, false) // Switched Off - .output(STATE_MACHINE, StateMachine.State.UNDEFINED)) // + .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-Undefined")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOn")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOn")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // // Ex; after long time if hard switch turned on.... .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOn")) // .input(BMS_CONTROL, true) // Switched On - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOn")) // - .input(BATTERY_RELAY, true) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .input("io0", INPUT_OUTPUT4, true) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase()// - .input(BATTERY_RELAY, true) // Switch ON + .input("io0", INPUT_OUTPUT4, true) // Switch ON .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayHold")) - .onAfterProcessImage(() -> clock.leap(11, ChronoUnit.SECONDS))) // + .onAfterProcessImage(() -> clock.leap(11, SECONDS))) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)); // + .output(STATE_MACHINE, State.GO_RUNNING)); // } @Test @@ -334,224 +306,221 @@ public void testGetHardwareTypeFromRegisterValue() { @Test public void testMinVoltageGoStopped() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); var sut = new BatteryFeneconHomeImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setStartStop(StartStopConfig.START) // .setBatteryStartUpRelay("io0/InputOutput4")// .build())// .next(new TestCase() // - .input(BATTERY_RELAY, false) // + .input("io0", INPUT_OUTPUT4, false) // .input(BMS_CONTROL, true) // Switched On - .output(STATE_MACHINE, StateMachine.State.UNDEFINED))// + .output(STATE_MACHINE, State.UNDEFINED))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-Undefined")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-RetryModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForBmsControl")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // - .output(STATE_MACHINE, StateMachine.State.RUNNING)) + .output(STATE_MACHINE, State.RUNNING)) /* * Critical min voltage */ .next(new TestCase("MinCellVoltage below critical value") // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .input(CURRENT, 0) // .output(LOW_MIN_VOLTAGE_WARNING, true) // .output(LOW_MIN_VOLTAGE_FAULT, false) // - .output(STATE_MACHINE, StateMachine.State.RUNNING) // + .output(STATE_MACHINE, State.RUNNING) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false) // - .onAfterControllersCallbacks( - () -> clock.leap(BatteryFeneconHomeImpl.TIMEOUT - 10, ChronoUnit.SECONDS))) // + .onAfterControllersCallbacks(() -> clock.leap(TIMEOUT - 10, SECONDS))) // .next(new TestCase("MinCellVoltage below critical value - charging resets time") // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .input(CURRENT, -300) // .output(LOW_MIN_VOLTAGE_WARNING, true) // .output(LOW_MIN_VOLTAGE_FAULT, false) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false)) // .next(new TestCase("MinCellVoltage below critical value - timer starts again") // .input(CURRENT, 0) // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .output(LOW_MIN_VOLTAGE_WARNING, true) // .output(LOW_MIN_VOLTAGE_FAULT, false) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false) // - .onAfterControllersCallbacks( - () -> clock.leap(BatteryFeneconHomeImpl.TIMEOUT - 10, ChronoUnit.SECONDS))) // + .onAfterControllersCallbacks(() -> clock.leap(TIMEOUT - 10, SECONDS))) // .next(new TestCase("MinCellVoltage below critical value - time not passed") // .input(CURRENT, 0) // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .output(LOW_MIN_VOLTAGE_WARNING, true) // .output(LOW_MIN_VOLTAGE_FAULT, false) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false) // - .onAfterControllersCallbacks(() -> clock.leap(15, ChronoUnit.SECONDS))) // + .onAfterControllersCallbacks(() -> clock.leap(15, SECONDS))) // .next(new TestCase("MinCellVoltage below critical value - time passed") // .input(CURRENT, 0) // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .output(LOW_MIN_VOLTAGE_FAULT, true) // .output(LOW_MIN_VOLTAGE_WARNING, false) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false) // - .output(STATE_MACHINE, StateMachine.State.RUNNING)) // + .output(STATE_MACHINE, State.RUNNING)) // .next(new TestCase("MinCellVoltage below critical value - error") // .input(LOW_MIN_VOLTAGE_FAULT, true) // .input(CURRENT, 0) // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .output(LOW_MIN_VOLTAGE_FAULT, true) // .output(LOW_MIN_VOLTAGE_WARNING, false) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false)) // .next(new TestCase() // - .output(STATE_MACHINE, StateMachine.State.ERROR)) // + .output(STATE_MACHINE, State.ERROR)) // .next(new TestCase("MinCellVoltage below critical value - go stopped") // .input(LOW_MIN_VOLTAGE_FAULT, true) // .input(CURRENT, 0) // // MinCellVoltage would be null, but there is not DummyTimedata for not to test // "getPastValues" - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .output(LOW_MIN_VOLTAGE_FAULT, true) // .output(LOW_MIN_VOLTAGE_WARNING, false) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false) // - .output(STATE_MACHINE, StateMachine.State.GO_STOPPED) // - .onAfterControllersCallbacks(() -> clock.leap(2_100, ChronoUnit.SECONDS))) // 35 minutes + .output(STATE_MACHINE, State.GO_STOPPED) // + .onAfterControllersCallbacks(() -> clock.leap(2_100, SECONDS))) // 35 minutes .next(new TestCase() // .input(MODBUS_COMMUNICATION_FAILED, true) // ) // .next(new TestCase("MinCellVoltage below critical value - stopped") // .input(CURRENT, 0) // .input(MODBUS_COMMUNICATION_FAILED, true) // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .output(LOW_MIN_VOLTAGE_WARNING, false) // .output(LOW_MIN_VOLTAGE_FAULT, false) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, true) // - .output(STATE_MACHINE, StateMachine.State.STOPPED) // + .output(STATE_MACHINE, State.STOPPED) // ); } @Test public void testMinVoltageCharging() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); var sut = new BatteryFeneconHomeImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setStartStop(StartStopConfig.START) // .setBatteryStartUpRelay("io0/InputOutput4")// .build())// .next(new TestCase() // - .input(BATTERY_RELAY, false) // + .input("io0", INPUT_OUTPUT4, false) // .input(BMS_CONTROL, true) // Switched On - .output(STATE_MACHINE, StateMachine.State.UNDEFINED))// + .output(STATE_MACHINE, State.UNDEFINED))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-Undefined")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-RetryModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForBmsControl")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // - .output(STATE_MACHINE, StateMachine.State.RUNNING)) + .output(STATE_MACHINE, State.RUNNING)) /* * Critical min voltage */ .next(new TestCase("MinCellVoltage below critical value") // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .input(CURRENT, 0) // .output(LOW_MIN_VOLTAGE_WARNING, true) // .output(LOW_MIN_VOLTAGE_FAULT, false) // - .output(STATE_MACHINE, StateMachine.State.RUNNING) // + .output(STATE_MACHINE, State.RUNNING) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false) // - .onAfterControllersCallbacks( - () -> clock.leap(BatteryFeneconHomeImpl.TIMEOUT - 10, ChronoUnit.SECONDS))) // + .onAfterControllersCallbacks(() -> clock.leap(TIMEOUT - 10, SECONDS))) // .next(new TestCase("MinCellVoltage below critical value - charging resets time") // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE - 100)) // .input(CURRENT, -300) // .output(LOW_MIN_VOLTAGE_WARNING, true) // .output(LOW_MIN_VOLTAGE_FAULT, false) // - .output(STATE_MACHINE, StateMachine.State.RUNNING) // + .output(STATE_MACHINE, State.RUNNING) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false)) // .next(new TestCase("MinCellVoltage below critical value - charging") // .input(CURRENT, -2000) // - .input(MIN_CELL_VOLTAGE, (BatteryFeneconHomeImpl.DEFAULT_CRITICAL_MIN_VOLTAGE + 50)) // + .input(MIN_CELL_VOLTAGE, (DEFAULT_CRITICAL_MIN_VOLTAGE + 50)) // .output(LOW_MIN_VOLTAGE_WARNING, false) // .output(LOW_MIN_VOLTAGE_FAULT, false) // - .output(STATE_MACHINE, StateMachine.State.RUNNING) // + .output(STATE_MACHINE, State.RUNNING) // .output(LOW_MIN_VOLTAGE_FAULT_BATTERY_STOPPED, false) // ); } @Test public void testNumberOfTowers() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); var sut = new BatteryFeneconHomeImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setStartStop(StartStopConfig.START) // - .setBatteryStartUpRelay("io0/InputOutput4")// - .build())// + .setBatteryStartUpRelay("io0/InputOutput4") // + .build()) // .next(new TestCase() // - .input(BATTERY_RELAY, false) // + .input("io0", INPUT_OUTPUT4, false) // .input(BMS_CONTROL, true) // Switched On - .output(STATE_MACHINE, StateMachine.State.UNDEFINED))// + .output(STATE_MACHINE, State.UNDEFINED))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-Undefined")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-RetryModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForBmsControl")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // - .output(STATE_MACHINE, StateMachine.State.RUNNING)) + .output(STATE_MACHINE, State.RUNNING)) .next(new TestCase() // .output(NUMBER_OF_TOWERS, null)) .next(new TestCase() // @@ -610,37 +579,37 @@ public void testBatteryProtectionSocLimitations() throws Exception { new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // - .addComponent(new DummyInputOutput(IO_ID))// + .addReference("setModbus", new DummyModbusBridge("modbus0")) // + .addComponent(new DummyInputOutput("io0"))// .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setStartStop(StartStopConfig.START) // .setBatteryStartUpRelay("io0/InputOutput4")// .build()) // .next(new TestCase() // - .input(BATTERY_RELAY, false) // + .input("io0", INPUT_OUTPUT4, false) // .input(BMS_CONTROL, true) // Switched On - .output(STATE_MACHINE, StateMachine.State.UNDEFINED))// + .output(STATE_MACHINE, State.UNDEFINED))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-Undefined")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-StartUpRelayOff")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-RetryModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING))// + .output(STATE_MACHINE, State.GO_RUNNING))// .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForBmsControl")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // .onBeforeProcessImage(assertLog(sut, "GoRunning-WaitForModbusCommunication")) // - .output(STATE_MACHINE, StateMachine.State.GO_RUNNING)) // + .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // - .output(STATE_MACHINE, StateMachine.State.RUNNING)) // + .output(STATE_MACHINE, State.RUNNING)) // .next(new TestCase() // .output(BP_CHARGE_MAX_SOC, 40)) // @@ -648,23 +617,45 @@ public void testBatteryProtectionSocLimitations() throws Exception { .input(SOC, 97) // .output(SOC, 97)) // .next(new TestCase() // - .output(BP_CHARGE_MAX_SOC, (int) Math.round(40 * 0.625))) // + .output(BP_CHARGE_MAX_SOC, round(40 * 0.625F))) // .next(new TestCase() // .input(SOC, 98)) // .next(new TestCase() // - .output(BP_CHARGE_MAX_SOC, (int) Math.round(40 * 0.4))) // + .output(BP_CHARGE_MAX_SOC, round(40 * 0.4F))) // .next(new TestCase() // .input(SOC, 99)) // .next(new TestCase() // - .output(BP_CHARGE_MAX_SOC, (int) Math.round(40 * 0.2))) // + .output(BP_CHARGE_MAX_SOC, round(40 * 0.2F))) // .next(new TestCase() // .input(SOC, 100)) // .next(new TestCase() // - .output(BP_CHARGE_MAX_SOC, (int) Math.round(40 * 0.05))) // + .output(BP_CHARGE_MAX_SOC, round(40 * 0.05F))) // .next(new TestCase() // .input(SOC, 99)) // .next(new TestCase() // - .output(BP_CHARGE_MAX_SOC, (int) Math.round(40 * 0.2)) // + .output(BP_CHARGE_MAX_SOC, round(40 * 0.2F)) // ); } + + @Test + public void testReadModbus() throws Exception { + var sut = new BatteryFeneconHomeImpl(); + new ComponentTest(sut) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("setModbus", new DummyModbusBridge("modbus0") // + .withRegister(18000, (byte) 0x00, (byte) 0x00)) // TOWER_4_BMS_SOFTWARE_VERSION + .activate(MyConfig.create() // + .setId("battery0") // + .setModbusId("modbus0") // + .setModbusUnitId(0) // + .setStartStop(StartStopConfig.START) // + .setBatteryStartUpRelay("io0/InputOutput4")// + .build()) // + + .next(new TestCase() // + .output(BatteryFeneconHome.ChannelId.TOWER_4_BMS_SOFTWARE_VERSION, 0)) // + + .deactivate(); + } } diff --git a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/TowersAndModulesTest.java b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/TowersAndModulesTest.java index ff8b7353731..9fff171c345 100644 --- a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/TowersAndModulesTest.java +++ b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/TowersAndModulesTest.java @@ -1,11 +1,15 @@ package io.openems.edge.battery.fenecon.home; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.BATTERY_HARDWARE_TYPE; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.NUMBER_OF_MODULES_PER_TOWER; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.TOWER_0_BMS_SOFTWARE_VERSION; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.TOWER_1_BMS_SOFTWARE_VERSION; +import static io.openems.edge.battery.fenecon.home.BatteryFeneconHome.ChannelId.TOWER_2_BMS_SOFTWARE_VERSION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.api.Battery; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.channel.ChannelId; @@ -17,19 +21,6 @@ public class TowersAndModulesTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - - private static final ChannelAddress NUMBER_OF_MODULES_PER_TOWER = new ChannelAddress(BATTERY_ID, - "NumberOfModulesPerTower"); - private static final ChannelAddress TOWER_0_BMS_SOFTWARE_VERSION = new ChannelAddress(BATTERY_ID, - "Tower0BmsSoftwareVersion"); - private static final ChannelAddress TOWER_1_BMS_SOFTWARE_VERSION = new ChannelAddress(BATTERY_ID, - "Tower1BmsSoftwareVersion"); - private static final ChannelAddress TOWER_2_BMS_SOFTWARE_VERSION = new ChannelAddress(BATTERY_ID, - "Tower2BmsSoftwareVersion"); - private static final ChannelAddress BATTERY_HARDWARE_TYPE = new ChannelAddress(BATTERY_ID, "BatteryHardwareType"); - private static final int TOWERS = 1; private static final int MODULES = 5; private static final int CELLS = 14; @@ -40,10 +31,10 @@ public void testChannelsCreatedDynamically() throws Exception { var componentTest = new ComponentTest(battery) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setBatteryStartUpRelay("io0/Relay4") // .setStartStop(StartStopConfig.AUTO) // diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/BatterySoltaroClusterVersionB.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/BatterySoltaroClusterVersionB.java index 1276244de00..867c5b99a9f 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/BatterySoltaroClusterVersionB.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/BatterySoltaroClusterVersionB.java @@ -52,80 +52,80 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .accessMode(AccessMode.READ_WRITE)), // // StateChannels - MASTER_ALARM_COMMUNICATION_ERROR_WITH_SUBMASTER(Doc.of(Level.FAULT) // + MASTER_ALARM_COMMUNICATION_ERROR_WITH_SUBMASTER(Doc.of(Level.WARNING) // .text("Communication error with submaster")), - MASTER_ALARM_PCS_EMS_COMMUNICATION_FAILURE(Doc.of(Level.FAULT) // + MASTER_ALARM_PCS_EMS_COMMUNICATION_FAILURE(Doc.of(Level.WARNING) // .text("PCS/EMS communication failure alarm")), - MASTER_ALARM_PCS_EMS_CONTROL_FAIL(Doc.of(Level.FAULT) // + MASTER_ALARM_PCS_EMS_CONTROL_FAIL(Doc.of(Level.WARNING) // .text("PCS/EMS control fail alarm")), MASTER_ALARM_LEVEL_1_INSULATION(Doc.of(Level.WARNING) // .text("System insulation alarm level 1")), - MASTER_ALARM_LEVEL_2_INSULATION(Doc.of(Level.FAULT) // + MASTER_ALARM_LEVEL_2_INSULATION(Doc.of(Level.WARNING) // .text("System insulation alarm level 2")), - RACK_1_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_1_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 1 Level 2 Alarm")), - RACK_1_PCS_CONTROL_FAULT(Doc.of(Level.FAULT) // + RACK_1_PCS_CONTROL_FAULT(Doc.of(Level.WARNING) // .text("Rack 1 PCS control fault")), - RACK_1_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.FAULT) // + RACK_1_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.WARNING) // .text("Rack 1 Communication with master error")), - RACK_1_DEVICE_ERROR(Doc.of(Level.FAULT) // + RACK_1_DEVICE_ERROR(Doc.of(Level.WARNING) // .text("Rack 1 Device error")), - RACK_1_CYCLE_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_1_CYCLE_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 1 Cycle over current")), - RACK_1_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_1_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 1 Voltage difference")), - RACK_2_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_2_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 2 Level 2 Alarm")), - RACK_2_PCS_CONTROL_FAULT(Doc.of(Level.FAULT) // + RACK_2_PCS_CONTROL_FAULT(Doc.of(Level.WARNING) // .text("Rack 2 PCS control fault")), - RACK_2_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.FAULT) // + RACK_2_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.WARNING) // .text("Rack 2 Communication with master error")), - RACK_2_DEVICE_ERROR(Doc.of(Level.FAULT) // + RACK_2_DEVICE_ERROR(Doc.of(Level.WARNING) // .text("Rack 2 Device error")), - RACK_2_CYCLE_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_2_CYCLE_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 1 Cycle over current")), - RACK_2_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_2_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 1 Voltage difference")), - RACK_3_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_3_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 3 Level 2 Alarm")), - RACK_3_PCS_CONTROL_FAULT(Doc.of(Level.FAULT) // + RACK_3_PCS_CONTROL_FAULT(Doc.of(Level.WARNING) // .text("Rack 3 PCS control fault")), - RACK_3_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.FAULT) // + RACK_3_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.WARNING) // .text("Rack 3 Communication with master error")), - RACK_3_DEVICE_ERROR(Doc.of(Level.FAULT) // + RACK_3_DEVICE_ERROR(Doc.of(Level.WARNING) // .text("Rack 3 Device error")), - RACK_3_CYCLE_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_3_CYCLE_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 1 Cycle over current")), - RACK_3_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_3_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 1 Voltage difference")), - RACK_4_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_4_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 4 Level 2 Alarm")), - RACK_4_PCS_CONTROL_FAULT(Doc.of(Level.FAULT) // + RACK_4_PCS_CONTROL_FAULT(Doc.of(Level.WARNING) // .text("Rack 4 PCS control fault")), - RACK_4_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.FAULT) // + RACK_4_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.WARNING) // .text("Rack 4 Communication with master error")), - RACK_4_DEVICE_ERROR(Doc.of(Level.FAULT) // + RACK_4_DEVICE_ERROR(Doc.of(Level.WARNING) // .text("Rack 4 Device error")), - RACK_4_CYCLE_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_4_CYCLE_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 1 Cycle over current")), - RACK_4_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_4_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 1 Voltage difference")), - RACK_5_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_5_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 5 Level 2 Alarm")), - RACK_5_PCS_CONTROL_FAULT(Doc.of(Level.FAULT) // + RACK_5_PCS_CONTROL_FAULT(Doc.of(Level.WARNING) // .text("Rack 5 PCS control fault")), - RACK_5_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.FAULT) // + RACK_5_COMMUNICATION_WITH_MASTER_ERROR(Doc.of(Level.WARNING) // .text("Rack 5 Communication with master error")), - RACK_5_DEVICE_ERROR(Doc.of(Level.FAULT) // + RACK_5_DEVICE_ERROR(Doc.of(Level.WARNING) // .text("Rack 5 Device error")), - RACK_5_CYCLE_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_5_CYCLE_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 1 Cycle over current")), - RACK_5_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_5_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 1 Voltage difference")),; private final Doc doc; diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/SingleRack.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/SingleRack.java index 45d65c88cfb..02835194b33 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/SingleRack.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/SingleRack.java @@ -439,28 +439,28 @@ private Map createChannelIdMap() { this.addEntry(map, KEY_MAX_CELL_TEMPERATURE, new IntegerDoc().unit(Unit.DEZIDEGREE_CELSIUS)); this.addEntry(map, KEY_MIN_CELL_TEMPERATURE_ID, new IntegerDoc().unit(Unit.NONE)); this.addEntry(map, KEY_MIN_CELL_TEMPERATURE, new IntegerDoc().unit(Unit.DEZIDEGREE_CELSIUS)); - this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_DISCHA_TEMP_LOW, Doc.of(Level.FAULT) + this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_DISCHA_TEMP_LOW, Doc.of(Level.WARNING) .text("Rack" + this.rackNumber + " Cell Discharge Temperature Low Alarm Level 2")); /* Bit 15 */ - this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_DISCHA_TEMP_HIGH, Doc.of(Level.FAULT) + this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_DISCHA_TEMP_HIGH, Doc.of(Level.WARNING) .text("Rack" + this.rackNumber + " Cell Discharge Temperature High Alarm Level 2")); /* Bit 14 */ this.addEntry(map, KEY_ALARM_LEVEL_2_GR_TEMPERATURE_HIGH, - Doc.of(Level.FAULT).text("Rack" + this.rackNumber + " GR Temperature High Alarm Level 2")); /* Bit 10 */ - this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_CHA_TEMP_LOW, Doc.of(Level.FAULT) + Doc.of(Level.WARNING).text("Rack" + this.rackNumber + " GR Temperature High Alarm Level 2")); /* Bit 10 */ + this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_CHA_TEMP_LOW, Doc.of(Level.WARNING) .text("Rack" + this.rackNumber + " Cell Charge Temperature Low Alarm Level 2")); /* Bit 7 */ - this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_CHA_TEMP_HIGH, Doc.of(Level.FAULT) + this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_CHA_TEMP_HIGH, Doc.of(Level.WARNING) .text("Rack" + this.rackNumber + " Cell Charge Temperature High Alarm Level 2")); /* Bit 6 */ - this.addEntry(map, KEY_ALARM_LEVEL_2_DISCHA_CURRENT_HIGH, Doc.of(Level.FAULT) + this.addEntry(map, KEY_ALARM_LEVEL_2_DISCHA_CURRENT_HIGH, Doc.of(Level.WARNING) .text("Rack" + this.rackNumber + " Discharge Current High Alarm Level 2")); /* Bit 5 */ this.addEntry(map, KEY_ALARM_LEVEL_2_TOTAL_VOLTAGE_LOW, - Doc.of(Level.FAULT).text("Rack" + this.rackNumber + " Total Voltage Low Alarm Level 2")); /* Bit 4 */ + Doc.of(Level.WARNING).text("Rack" + this.rackNumber + " Total Voltage Low Alarm Level 2")); /* Bit 4 */ this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_VOLTAGE_LOW, - Doc.of(Level.FAULT).text("Cluster 1 Cell Voltage Low Alarm Level 2")); /* Bit 3 */ + Doc.of(Level.WARNING).text("Cluster 1 Cell Voltage Low Alarm Level 2")); /* Bit 3 */ this.addEntry(map, KEY_ALARM_LEVEL_2_CHA_CURRENT_HIGH, - Doc.of(Level.FAULT).text("Rack" + this.rackNumber + " Charge Current High Alarm Level 2")); /* Bit 2 */ + Doc.of(Level.WARNING).text("Rack" + this.rackNumber + " Charge Current High Alarm Level 2")); /* Bit 2 */ this.addEntry(map, KEY_ALARM_LEVEL_2_TOTAL_VOLTAGE_HIGH, - Doc.of(Level.FAULT).text("Rack" + this.rackNumber + " Total Voltage High Alarm Level 2")); /* Bit 1 */ + Doc.of(Level.WARNING).text("Rack" + this.rackNumber + " Total Voltage High Alarm Level 2")); /* Bit 1 */ this.addEntry(map, KEY_ALARM_LEVEL_2_CELL_VOLTAGE_HIGH, - Doc.of(Level.FAULT).text("Rack" + this.rackNumber + " Cell Voltage High Alarm Level 2")); /* Bit 0 */ + Doc.of(Level.WARNING).text("Rack" + this.rackNumber + " Cell Voltage High Alarm Level 2")); /* Bit 0 */ this.addEntry(map, KEY_ALARM_LEVEL_1_CELL_DISCHA_TEMP_LOW, Doc.of(Level.WARNING) .text("Rack" + this.rackNumber + " Cell Discharge Temperature Low Alarm Level 1")); /* Bit 15 */ this.addEntry(map, KEY_ALARM_LEVEL_1_CELL_DISCHA_TEMP_HIGH, Doc.of(Level.WARNING) @@ -492,23 +492,23 @@ private Map createChannelIdMap() { this.addEntry(map, KEY_ALARM_LEVEL_1_CELL_VOLTAGE_HIGH, Doc.of(Level.WARNING).text("Rack" + this.rackNumber + " Cell Voltage High Alarm Level 1")); /* Bit 0 */ this.addEntry(map, KEY_RUN_STATE, Doc.of(Enums.ClusterRunState.values())); // - this.addEntry(map, KEY_FAILURE_INITIALIZATION, Doc.of(Level.FAULT).text("Initialization failure")); /* Bit */ - this.addEntry(map, KEY_FAILURE_EEPROM, Doc.of(Level.FAULT).text("EEPROM fault")); /* Bit 11 */ + this.addEntry(map, KEY_FAILURE_INITIALIZATION, Doc.of(Level.WARNING).text("Initialization failure")); /* Bit */ + this.addEntry(map, KEY_FAILURE_EEPROM, Doc.of(Level.WARNING).text("EEPROM fault")); /* Bit 11 */ this.addEntry(map, KEY_FAILURE_INTRANET_COMMUNICATION, - Doc.of(Level.FAULT).text("Internal communication fault")); /* Bit 10 */ + Doc.of(Level.WARNING).text("Internal communication fault")); /* Bit 10 */ this.addEntry(map, KEY_FAILURE_TEMPERATURE_SENSOR_CABLE, - Doc.of(Level.FAULT).text("Temperature sensor cable fault")); /* Bit 9 */ + Doc.of(Level.WARNING).text("Temperature sensor cable fault")); /* Bit 9 */ this.addEntry(map, KEY_FAILURE_BALANCING_MODULE, Doc.of(Level.OK).text("Balancing module fault")); /* Bit 8 */ - this.addEntry(map, KEY_FAILURE_TEMPERATURE_PCB, Doc.of(Level.FAULT).text("Temperature PCB error")); /* Bit 7 */ - this.addEntry(map, KEY_FAILURE_GR_TEMPERATURE, Doc.of(Level.FAULT).text("GR Temperature error")); /* Bit 6 */ - this.addEntry(map, KEY_FAILURE_TEMP_SENSOR, Doc.of(Level.FAULT).text("Temperature sensor fault")); /* Bit 5 */ + this.addEntry(map, KEY_FAILURE_TEMPERATURE_PCB, Doc.of(Level.WARNING).text("Temperature PCB error")); /* Bit 7 */ + this.addEntry(map, KEY_FAILURE_GR_TEMPERATURE, Doc.of(Level.WARNING).text("GR Temperature error")); /* Bit 6 */ + this.addEntry(map, KEY_FAILURE_TEMP_SENSOR, Doc.of(Level.WARNING).text("Temperature sensor fault")); /* Bit 5 */ this.addEntry(map, KEY_FAILURE_TEMP_SAMPLING, - Doc.of(Level.FAULT).text("Temperature sampling fault")); /* Bit 4 */ + Doc.of(Level.WARNING).text("Temperature sampling fault")); /* Bit 4 */ this.addEntry(map, KEY_FAILURE_VOLTAGE_SAMPLING, - Doc.of(Level.FAULT).text("Voltage sampling fault")); /* Bit 3 */ - this.addEntry(map, KEY_FAILURE_LTC6803, Doc.of(Level.FAULT).text("LTC6803 fault")); /* Bit 2 */ - this.addEntry(map, KEY_FAILURE_CONNECTOR_WIRE, Doc.of(Level.FAULT).text("connector wire fault")); /* Bit 1 */ - this.addEntry(map, KEY_FAILURE_SAMPLING_WIRE, Doc.of(Level.FAULT).text("sampling wire fault")); /* Bit 0 */ + Doc.of(Level.WARNING).text("Voltage sampling fault")); /* Bit 3 */ + this.addEntry(map, KEY_FAILURE_LTC6803, Doc.of(Level.WARNING).text("LTC6803 fault")); /* Bit 2 */ + this.addEntry(map, KEY_FAILURE_CONNECTOR_WIRE, Doc.of(Level.WARNING).text("connector wire fault")); /* Bit 1 */ + this.addEntry(map, KEY_FAILURE_SAMPLING_WIRE, Doc.of(Level.WARNING).text("sampling wire fault")); /* Bit 0 */ this.addEntry(map, KEY_SLEEP, Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_WRITE)); this.addEntry(map, KEY_RESET, Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_WRITE)); diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionc/BatterySoltaroClusterVersionC.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionc/BatterySoltaroClusterVersionC.java index 99f8fea4c9f..cec303bdbf7 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionc/BatterySoltaroClusterVersionC.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionc/BatterySoltaroClusterVersionC.java @@ -265,82 +265,82 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId // Master BMS Alarm Registers MASTER_EMS_COMMUNICATION_FAILURE(Doc.of(Level.WARNING) // .text("Master EMS Communication Failure")), - MASTER_PCS_CONTROL_FAILURE(Doc.of(Level.FAULT) // + MASTER_PCS_CONTROL_FAILURE(Doc.of(Level.WARNING) // .text("Master PCS Control Failure")), - MASTER_PCS_COMMUNICATION_FAILURE(Doc.of(Level.FAULT) // + MASTER_PCS_COMMUNICATION_FAILURE(Doc.of(Level.WARNING) // .text("Master PCS Communication Failure")), // Rack #1 cannot be paralleled to DC Bus reasons - RACK_1_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_1_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 1 Level 2 Alarm")), - RACK_1_PCS_CONTROL_FAILURE(Doc.of(Level.FAULT) // + RACK_1_PCS_CONTROL_FAILURE(Doc.of(Level.WARNING) // .text("Rack 1 PCS Control Failure")), - RACK_1_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.FAULT) // + RACK_1_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.WARNING) // .text("Rack 1 Communication to Master BMS Failure")), - RACK_1_HARDWARE_FAILURE(Doc.of(Level.FAULT) // + RACK_1_HARDWARE_FAILURE(Doc.of(Level.WARNING) // .text("Rack 1 Hardware Failure")), - RACK_1_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_1_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 1 Too big circulating Current among clusters (>4A)")), - RACK_1_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_1_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 1 Too big boltage difference among clusters (>50V)")), // Rack #2 cannot be paralleled to DC Bus reasons - RACK_2_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_2_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 2 Level 2 Alarm")), - RACK_2_PCS_CONTROL_FAILURE(Doc.of(Level.FAULT) // + RACK_2_PCS_CONTROL_FAILURE(Doc.of(Level.WARNING) // .text("Rack 2 PCS Control Failure")), - RACK_2_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.FAULT) // + RACK_2_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.WARNING) // .text("Rack 2 Communication to Master BMS Failure")), - RACK_2_HARDWARE_FAILURE(Doc.of(Level.FAULT) // + RACK_2_HARDWARE_FAILURE(Doc.of(Level.WARNING) // .text("Rack 2 Hardware Failure")), - RACK_2_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_2_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 2 Too big circulating Current among clusters (>4A)")), - RACK_2_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_2_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 2 Too big boltage difference among clusters (>50V)")), // Rack #3 cannot be paralleled to DC Bus reasons - RACK_3_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_3_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 3 Level 2 Alarm")), - RACK_3_PCS_CONTROL_FAILURE(Doc.of(Level.FAULT) // + RACK_3_PCS_CONTROL_FAILURE(Doc.of(Level.WARNING) // .text("Rack 3 PCS Control Failure")), - RACK_3_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.FAULT) // + RACK_3_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.WARNING) // .text("Rack 3 Communication to Master BMS Failure")), - RACK_3_HARDWARE_FAILURE(Doc.of(Level.FAULT) // + RACK_3_HARDWARE_FAILURE(Doc.of(Level.WARNING) // .text("Rack 3 Hardware Failure")), - RACK_3_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_3_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 3 Too big circulating Current among clusters (>4A)")), - RACK_3_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_3_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 3 Too big boltage difference among clusters (>50V)")), // Rack #4 cannot be paralleled to DC Bus reasons - RACK_4_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_4_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 4 Level 2 Alarm")), - RACK_4_PCS_CONTROL_FAILURE(Doc.of(Level.FAULT) // + RACK_4_PCS_CONTROL_FAILURE(Doc.of(Level.WARNING) // .text("Rack 4 PCS Control Failure")), - RACK_4_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.FAULT) // + RACK_4_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.WARNING) // .text("Rack 4 Communication to Master BMS Failure")), - RACK_4_HARDWARE_FAILURE(Doc.of(Level.FAULT) // + RACK_4_HARDWARE_FAILURE(Doc.of(Level.WARNING) // .text("Rack 4 Hardware Failure")), - RACK_4_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_4_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 4 Too big circulating Current among clusters (>4A)")), - RACK_4_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_4_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 4 Too big boltage difference among clusters (>50V)")), // Rack #5 cannot be paralleled to DC Bus reasons - RACK_5_LEVEL_2_ALARM(Doc.of(Level.FAULT) // + RACK_5_LEVEL_2_ALARM(Doc.of(Level.WARNING) // .text("Rack 5 Level 2 Alarm")), - RACK_5_PCS_CONTROL_FAILURE(Doc.of(Level.FAULT) // + RACK_5_PCS_CONTROL_FAILURE(Doc.of(Level.WARNING) // .text("Rack 5 PCS Control Failure")), - RACK_5_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.FAULT) // + RACK_5_COMMUNICATION_TO_MASTER_FAILURE(Doc.of(Level.WARNING) // .text("Rack 5 Communication to Master BMS Failure")), - RACK_5_HARDWARE_FAILURE(Doc.of(Level.FAULT) // + RACK_5_HARDWARE_FAILURE(Doc.of(Level.WARNING) // .text("Rack 5 Hardware Failure")), - RACK_5_OVER_CURRENT(Doc.of(Level.FAULT) // + RACK_5_OVER_CURRENT(Doc.of(Level.WARNING) // .text("Rack 5 Too big circulating Current among clusters (>4A)")), - RACK_5_VOLTAGE_DIFFERENCE(Doc.of(Level.FAULT) // + RACK_5_VOLTAGE_DIFFERENCE(Doc.of(Level.WARNING) // .text("Rack 5 Too big boltage difference among clusters (>50V)")), // OpenEMS Faults - RUN_FAILED(Doc.of(Level.FAULT) // + RUN_FAILED(Doc.of(Level.WARNING) // .text("Running the Logic failed")), // - MAX_START_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_START_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of start attempts failed")), // - MAX_STOP_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_STOP_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of stop attempts failed")), // NUMBER_OF_MODULES_PER_TOWER(Doc.of(OpenemsType.INTEGER) // .persistencePriority(PersistencePriority.HIGH) // diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionc/RackChannel.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionc/RackChannel.java index ec13258ac7c..daee5253fa6 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionc/RackChannel.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionc/RackChannel.java @@ -463,27 +463,27 @@ public enum RackChannel { LEVEL1_TOTAL_VOLTAGE_HIGH(Doc.of(Level.WARNING) // .text("Total Voltage High Alarm Level 1")), // // Alarm Level 2 - LEVEL2_DISCHARGE_TEMP_LOW(Doc.of(Level.FAULT) // + LEVEL2_DISCHARGE_TEMP_LOW(Doc.of(Level.WARNING) // .text("Discharge Temperature Low Alarm Level 2")), // - LEVEL2_DISCHARGE_TEMP_HIGH(Doc.of(Level.FAULT) // + LEVEL2_DISCHARGE_TEMP_HIGH(Doc.of(Level.WARNING) // .text("Discharge Temperature High Alarm Level 2")), // - LEVEL2_INSULATION_VALUE(Doc.of(Level.FAULT) // + LEVEL2_INSULATION_VALUE(Doc.of(Level.WARNING) // .text("Insulation Value Failure Alarm Level 2")), // - LEVEL2_POWER_POLE_TEMP_HIGH(Doc.of(Level.FAULT) // + LEVEL2_POWER_POLE_TEMP_HIGH(Doc.of(Level.WARNING) // .text("Power Pole temperature too high Alarm Level 2")), // - LEVEL2_CHARGE_TEMP_LOW(Doc.of(Level.FAULT) // + LEVEL2_CHARGE_TEMP_LOW(Doc.of(Level.WARNING) // .text("Cell Charge Temperature Low Alarm Level 2")), // - LEVEL2_CHARGE_TEMP_HIGH(Doc.of(Level.FAULT) // + LEVEL2_CHARGE_TEMP_HIGH(Doc.of(Level.WARNING) // .text("Charge Temperature High Alarm Level 2")), // - LEVEL2_DISCHARGE_CURRENT_HIGH(Doc.of(Level.FAULT) // + LEVEL2_DISCHARGE_CURRENT_HIGH(Doc.of(Level.WARNING) // .text("Discharge Current High Alarm Level 2")), // - LEVEL2_TOTAL_VOLTAGE_LOW(Doc.of(Level.FAULT) // + LEVEL2_TOTAL_VOLTAGE_LOW(Doc.of(Level.WARNING) // .text("Total Voltage Low Alarm Level 2")), // - LEVEL2_CELL_VOLTAGE_LOW(Doc.of(Level.FAULT) // + LEVEL2_CELL_VOLTAGE_LOW(Doc.of(Level.WARNING) // .text("Cell Voltage Low Alarm Level 2")), // - LEVEL2_CHARGE_CURRENT_HIGH(Doc.of(Level.FAULT) // + LEVEL2_CHARGE_CURRENT_HIGH(Doc.of(Level.WARNING) // .text("Charge Current High Alarm Level 2")), // - LEVEL2_TOTAL_VOLTAGE_HIGH(Doc.of(Level.FAULT) // + LEVEL2_TOTAL_VOLTAGE_HIGH(Doc.of(Level.WARNING) // .text("Total Voltage High Alarm Level 2")), // LEVEL2_CELL_VOLTAGE_HIGH(Doc.of(Level.INFO) // .text("Cell Voltage High Alarm Level 2")), // diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versiona/BatterySoltaroSingleRackVersionA.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versiona/BatterySoltaroSingleRackVersionA.java index 9ee4f1c6abf..7d65c2aacd1 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versiona/BatterySoltaroSingleRackVersionA.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versiona/BatterySoltaroSingleRackVersionA.java @@ -681,29 +681,29 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .text("Cluster 1 Total Voltage High Alarm Level 1")), // ALARM_LEVEL_1_CELL_VOLTAGE_HIGH(Doc.of(Level.WARNING) // .text("Cluster 1 Cell Voltage High Alarm Level 1")), // - FAILURE_INITIALIZATION(Doc.of(Level.FAULT) // + FAILURE_INITIALIZATION(Doc.of(Level.WARNING) // .text("Initialization failure")), // - FAILURE_EEPROM(Doc.of(Level.FAULT) // + FAILURE_EEPROM(Doc.of(Level.WARNING) // .text("EEPROM fault")), // - FAILURE_INTRANET_COMMUNICATION(Doc.of(Level.FAULT) // + FAILURE_INTRANET_COMMUNICATION(Doc.of(Level.WARNING) // .text("Intranet communication fault")), // - FAILURE_TEMP_SAMPLING_LINE(Doc.of(Level.FAULT) // + FAILURE_TEMP_SAMPLING_LINE(Doc.of(Level.WARNING) // .text("Temperature sampling line fault")), // - FAILURE_BALANCING_MODULE(Doc.of(Level.FAULT) // + FAILURE_BALANCING_MODULE(Doc.of(Level.WARNING) // .text("Balancing module fault")), // - FAILURE_TEMP_SENSOR(Doc.of(Level.FAULT) // + FAILURE_TEMP_SENSOR(Doc.of(Level.WARNING) // .text("Temperature sensor fault")), // - FAILURE_TEMP_SAMPLING(Doc.of(Level.FAULT) // + FAILURE_TEMP_SAMPLING(Doc.of(Level.WARNING) // .text("Temperature sampling fault")), // - FAILURE_VOLTAGE_SAMPLING(Doc.of(Level.FAULT) // + FAILURE_VOLTAGE_SAMPLING(Doc.of(Level.WARNING) // .text("Voltage sampling fault")), // - FAILURE_LTC6803(Doc.of(Level.FAULT) // + FAILURE_LTC6803(Doc.of(Level.WARNING) // .text("LTC6803 fault")), // FAILURE_CONNECTOR_WIRE(Doc.of(Level.WARNING) // .text("connector wire fault")), // - FAILURE_SAMPLING_WIRE(Doc.of(Level.FAULT) // + FAILURE_SAMPLING_WIRE(Doc.of(Level.WARNING) // .text("sampling wire fault")), // - PRECHARGE_TAKING_TOO_LONG(Doc.of(Level.FAULT) // + PRECHARGE_TAKING_TOO_LONG(Doc.of(Level.WARNING) // .text("precharge time was too long")), // STATE_MACHINE(Doc.of(State.values()) // .text("Current State of State-Machine")), // diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/BatterySoltaroSingleRackVersionB.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/BatterySoltaroSingleRackVersionB.java index ecc741e2cc2..8434481d85c 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/BatterySoltaroSingleRackVersionB.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/BatterySoltaroSingleRackVersionB.java @@ -995,11 +995,11 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId .text("precharge time was too long")), // OpenEMS Faults - RUN_FAILED(Doc.of(Level.FAULT) // + RUN_FAILED(Doc.of(Level.WARNING) // .text("Running the Logic failed")), // - MAX_START_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_START_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of start attempts failed")), // - MAX_STOP_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_STOP_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of stop attempts failed")), // ; diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionc/BatterySoltaroSingleRackVersionC.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionc/BatterySoltaroSingleRackVersionC.java index 28a54d5b9af..1f57a137c60 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionc/BatterySoltaroSingleRackVersionC.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionc/BatterySoltaroSingleRackVersionC.java @@ -514,29 +514,29 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId // Faults and warnings // Alarm Level 2 - LEVEL2_DISCHARGE_TEMP_LOW(Doc.of(Level.FAULT) // + LEVEL2_DISCHARGE_TEMP_LOW(Doc.of(Level.WARNING) // .text("Discharge Temperature Low Alarm Level 2")), // - LEVEL2_DISCHARGE_TEMP_HIGH(Doc.of(Level.FAULT) // + LEVEL2_DISCHARGE_TEMP_HIGH(Doc.of(Level.WARNING) // .text("Discharge Temperature High Alarm Level 2")), // - LEVEL2_INSULATION_VALUE(Doc.of(Level.FAULT) // + LEVEL2_INSULATION_VALUE(Doc.of(Level.WARNING) // .text("Insulation Value Failure Alarm Level 2")), // - LEVEL2_POWER_POLE_TEMP_HIGH(Doc.of(Level.FAULT) // + LEVEL2_POWER_POLE_TEMP_HIGH(Doc.of(Level.WARNING) // .text("Power Pole temperature too high Alarm Level 2")), // - LEVEL2_CHARGE_TEMP_LOW(Doc.of(Level.FAULT) // + LEVEL2_CHARGE_TEMP_LOW(Doc.of(Level.WARNING) // .text("Cell Charge Temperature Low Alarm Level 2")), // - LEVEL2_CHARGE_TEMP_HIGH(Doc.of(Level.FAULT) // + LEVEL2_CHARGE_TEMP_HIGH(Doc.of(Level.WARNING) // .text("Charge Temperature High Alarm Level 2")), // - LEVEL2_DISCHARGE_CURRENT_HIGH(Doc.of(Level.FAULT) // + LEVEL2_DISCHARGE_CURRENT_HIGH(Doc.of(Level.WARNING) // .text("Discharge Current High Alarm Level 2")), // - LEVEL2_TOTAL_VOLTAGE_LOW(Doc.of(Level.FAULT) // + LEVEL2_TOTAL_VOLTAGE_LOW(Doc.of(Level.WARNING) // .text("Total Voltage Low Alarm Level 2")), // - LEVEL2_CELL_VOLTAGE_LOW(Doc.of(Level.FAULT) // + LEVEL2_CELL_VOLTAGE_LOW(Doc.of(Level.WARNING) // .text("Cell Voltage Low Alarm Level 2")), // - LEVEL2_CHARGE_CURRENT_HIGH(Doc.of(Level.FAULT) // + LEVEL2_CHARGE_CURRENT_HIGH(Doc.of(Level.WARNING) // .text("Charge Current High Alarm Level 2")), // - LEVEL2_TOTAL_VOLTAGE_HIGH(Doc.of(Level.FAULT) // + LEVEL2_TOTAL_VOLTAGE_HIGH(Doc.of(Level.WARNING) // .text("Total Voltage High Alarm Level 2")), // - LEVEL2_CELL_VOLTAGE_HIGH(Doc.of(Level.FAULT) // + LEVEL2_CELL_VOLTAGE_HIGH(Doc.of(Level.WARNING) // .text("Cell Voltage High Alarm Level 2")), // // Alarm Level 1 @@ -672,11 +672,11 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId .text("Slave 20 communication error")), // // OpenEMS Faults - RUN_FAILED(Doc.of(Level.FAULT) // + RUN_FAILED(Doc.of(Level.WARNING) // .text("Running the Logic failed")), // - MAX_START_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_START_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of start attempts failed")), // - MAX_STOP_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_STOP_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of stop attempts failed")), // ; diff --git a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/cluster/versionb/BatterySoltaroClusterVersionBImplTest.java b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/cluster/versionb/BatterySoltaroClusterVersionBImplTest.java index eda7ce07667..be7f6db76ce 100644 --- a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/cluster/versionb/BatterySoltaroClusterVersionBImplTest.java +++ b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/cluster/versionb/BatterySoltaroClusterVersionBImplTest.java @@ -1,9 +1,13 @@ package io.openems.edge.battery.soltaro.cluster.versionb; +import static io.openems.edge.battery.soltaro.cluster.SoltaroCluster.ChannelId.SUB_MASTER_1_COMMUNICATION_FAILURE; +import static io.openems.edge.battery.soltaro.cluster.SoltaroCluster.ChannelId.SUB_MASTER_2_COMMUNICATION_FAILURE; +import static io.openems.edge.battery.soltaro.cluster.SoltaroCluster.ChannelId.SUB_MASTER_3_COMMUNICATION_FAILURE; +import static io.openems.edge.battery.soltaro.cluster.SoltaroCluster.ChannelId.SUB_MASTER_4_COMMUNICATION_FAILURE; +import static io.openems.edge.battery.soltaro.cluster.SoltaroCluster.ChannelId.SUB_MASTER_5_COMMUNICATION_FAILURE; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; -import io.openems.edge.battery.soltaro.cluster.SoltaroCluster; import io.openems.edge.battery.soltaro.common.enums.BatteryState; import io.openems.edge.battery.soltaro.common.enums.ModuleType; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; @@ -14,30 +18,16 @@ public class BatterySoltaroClusterVersionBImplTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - - private static final ChannelAddress SUB_MASTER_1_COMMUNICATION_FAILURE = new ChannelAddress(BATTERY_ID, - SoltaroCluster.ChannelId.SUB_MASTER_1_COMMUNICATION_FAILURE.id()); - private static final ChannelAddress SUB_MASTER_2_COMMUNICATION_FAILURE = new ChannelAddress(BATTERY_ID, - SoltaroCluster.ChannelId.SUB_MASTER_2_COMMUNICATION_FAILURE.id()); - private static final ChannelAddress SUB_MASTER_3_COMMUNICATION_FAILURE = new ChannelAddress(BATTERY_ID, - SoltaroCluster.ChannelId.SUB_MASTER_3_COMMUNICATION_FAILURE.id()); - private static final ChannelAddress SUB_MASTER_4_COMMUNICATION_FAILURE = new ChannelAddress(BATTERY_ID, - SoltaroCluster.ChannelId.SUB_MASTER_4_COMMUNICATION_FAILURE.id()); - private static final ChannelAddress SUB_MASTER_5_COMMUNICATION_FAILURE = new ChannelAddress(BATTERY_ID, - SoltaroCluster.ChannelId.SUB_MASTER_5_COMMUNICATION_FAILURE.id()); - @Test public void test() throws Exception { var sut = new BatterySoltaroClusterVersionBImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setNumberOfSlaves(0) // .setModuleType(ModuleType.MODULE_3_5_KWH) // diff --git a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/cluster/versionc/BatterySoltaroClusterVersionCImplTest.java b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/cluster/versionc/BatterySoltaroClusterVersionCImplTest.java index aaffbc2cc9e..ebcb21122ce 100644 --- a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/cluster/versionc/BatterySoltaroClusterVersionCImplTest.java +++ b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/cluster/versionc/BatterySoltaroClusterVersionCImplTest.java @@ -9,17 +9,14 @@ public class BatterySoltaroClusterVersionCImplTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new BatterySoltaroClusterVersionCImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setStartStop(StartStopConfig.AUTO) // .build()) // ; diff --git a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versiona/BatterySoltaroSingleRackVersionAImplTest.java b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versiona/BatterySoltaroSingleRackVersionAImplTest.java index 443dbce24c9..daccfa35f66 100644 --- a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versiona/BatterySoltaroSingleRackVersionAImplTest.java +++ b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versiona/BatterySoltaroSingleRackVersionAImplTest.java @@ -9,17 +9,14 @@ public class BatterySoltaroSingleRackVersionAImplTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new BatterySoltaroSingleRackVersionAImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setErrorLevel2Delay(0) // .setMaxStartTime(0) // diff --git a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionb/BatterySoltaroSingleRackVersionBImplTest.java b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionb/BatterySoltaroSingleRackVersionBImplTest.java index b2b33ff92fb..3fce33da342 100644 --- a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionb/BatterySoltaroSingleRackVersionBImplTest.java +++ b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionb/BatterySoltaroSingleRackVersionBImplTest.java @@ -11,18 +11,15 @@ public class BatterySoltaroSingleRackVersionBImplTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new BatterySoltaroSingleRackVersionBImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setErrorLevel2Delay(0) // .setMaxStartTime(0) // diff --git a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionc/BatterySoltaroSingleRackVersionCImplTest.java b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionc/BatterySoltaroSingleRackVersionCImplTest.java index 82e9a8c75d9..504d8d66b2a 100644 --- a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionc/BatterySoltaroSingleRackVersionCImplTest.java +++ b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionc/BatterySoltaroSingleRackVersionCImplTest.java @@ -9,17 +9,14 @@ public class BatterySoltaroSingleRackVersionCImplTest { - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new BatterySoltaroSingleRackVersionCImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_ID) // - .setModbusId(MODBUS_ID) // + .setId("battery0") // + .setModbusId("modbus0") // .setModbusUnitId(0) // .setStartStop(StartStopConfig.AUTO) // .build()) // diff --git a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsave.java b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsave.java index d9124765b01..b32ba021cd3 100644 --- a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsave.java +++ b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsave.java @@ -33,29 +33,40 @@ public interface BatteryInverterKacoBlueplanetGridsave extends ManagedSymmetricB public static final int WATCHDOG_TRIGGER_SECONDS = 10; public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + + /* + * Whenever one of these states would be Level.FAULT, the EssGeneric will stop + * the battery and the inverter. If this is necessary, it must be specifically + * mentioned and the state should have a proper description of the fault. + */ + STATE_MACHINE(Doc.of(State.values()) // .text("Current State of State-Machine")), // - RUN_FAILED(Doc.of(Level.FAULT) // + RUN_FAILED(Doc.of(Level.WARNING) // .text("Running the Logic failed")), // - MAX_START_TIMEOUT(Doc.of(Level.FAULT) // + MAX_START_TIMEOUT(Doc.of(Level.WARNING) // .text("Max start time is exceeded")), // - MAX_STOP_TIMEOUT(Doc.of(Level.FAULT) // + MAX_STOP_TIMEOUT(Doc.of(Level.WARNING) // .text("Max stop time is exceeded")), // - INVERTER_CURRENT_STATE_FAULT(Doc.of(Level.FAULT) // + + /** + * Internal StateMachine from KACO. + */ + INVERTER_CURRENT_STATE_FAULT(Doc.of(Level.WARNING) // .text("The 'CurrentState' is invalid")), // - GRID_DISCONNECTION(Doc.of(Level.FAULT) // + GRID_DISCONNECTION(Doc.of(Level.WARNING) // .text("External grid protection disconnection (17)")), // - GRID_FAILURE_LINE_TO_LINE(Doc.of(Level.FAULT) // + GRID_FAILURE_LINE_TO_LINE(Doc.of(Level.WARNING) // .text("Grid failure phase-to-phase voltage (47)")), // - LINE_FAILURE_UNDER_FREQ(Doc.of(Level.FAULT) // + LINE_FAILURE_UNDER_FREQ(Doc.of(Level.WARNING) // .text("Line failure: Grid frequency is too low (48)")), // - LINE_FAILURE_OVER_FREQ(Doc.of(Level.FAULT) // + LINE_FAILURE_OVER_FREQ(Doc.of(Level.WARNING) // .text("Line failure: Grid frequency is too high (49)")), // - PROTECTION_SHUTDOWN_LINE_1(Doc.of(Level.FAULT) // + PROTECTION_SHUTDOWN_LINE_1(Doc.of(Level.WARNING) // .text("Grid Failure: grid voltage L1 protection (81)")), // - PROTECTION_SHUTDOWN_LINE_2(Doc.of(Level.FAULT) // + PROTECTION_SHUTDOWN_LINE_2(Doc.of(Level.WARNING) // .text("Grid Failure: grid voltage L2 protection (82)")), // - PROTECTION_SHUTDOWN_LINE_3(Doc.of(Level.FAULT) // + PROTECTION_SHUTDOWN_LINE_3(Doc.of(Level.WARNING) // .text("Grid Failure: grid voltage L3 protection (83)")), // ; diff --git a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImpl.java b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImpl.java index 84ec0153305..9b5a3cefef3 100644 --- a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImpl.java +++ b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImpl.java @@ -28,6 +28,7 @@ import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; +import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.common.types.OptionsEnum; @@ -57,6 +58,9 @@ import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.modbusslave.ModbusSlave; +import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; +import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.startstop.StartStop; import io.openems.edge.common.startstop.StartStoppable; import io.openems.edge.common.taskmanager.Priority; @@ -75,7 +79,7 @@ ) public class BatteryInverterKacoBlueplanetGridsaveImpl extends AbstractSunSpecBatteryInverter implements BatteryInverterKacoBlueplanetGridsave, ManagedSymmetricBatteryInverter, SymmetricBatteryInverter, - ModbusComponent, OpenemsComponent, TimedataProvider, StartStoppable { + ModbusComponent, ModbusSlave, OpenemsComponent, TimedataProvider, StartStoppable { private static final int UNIT_ID = 1; private static final int READ_FROM_MODBUS_BLOCK = 1; @@ -140,7 +144,6 @@ protected void setModbus(BridgeModbus modbus) { // .put(SunSpecModel.S_136, Priority.LOW) // // .put(SunSpecModel.S_160, Priority.LOW) // - @Activate public BatteryInverterKacoBlueplanetGridsaveImpl() { super(// ACTIVE_MODELS, // @@ -158,11 +161,11 @@ public BatteryInverterKacoBlueplanetGridsaveImpl() { @Activate private void activate(ComponentContext context, Config config) throws OpenemsException { + this.config = config; if (super.activate(context, config.id(), config.alias(), config.enabled(), UNIT_ID, this.cm, "Modbus", config.modbus_id(), READ_FROM_MODBUS_BLOCK)) { return; } - this.config = config; } @Override @@ -279,6 +282,16 @@ private void handleGridDisconnection() { }); } + @Override + public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { + return new ModbusSlaveTable(// + OpenemsComponent.getModbusSlaveNatureTable(accessMode), // + SymmetricBatteryInverter.getModbusSlaveNatureTable(accessMode), // + ManagedSymmetricBatteryInverter.getModbusSlaveNatureTable(accessMode), // + ModbusSlaveNatureTable.of(BatteryInverterKacoBlueplanetGridsave.class, accessMode, 100) // + .build()); + } + @Override public BatteryInverterConstraint[] getStaticConstraints() throws OpenemsException { if (this.stateMachine.getCurrentState() == State.RUNNING) { diff --git a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/KacoSunSpecModel.java b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/KacoSunSpecModel.java index 2757dbf6616..88974aeedf0 100644 --- a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/KacoSunSpecModel.java +++ b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/KacoSunSpecModel.java @@ -106,7 +106,8 @@ public static enum S64201 implements SunSpecPoint { V_AR(new ScaledValuePoint("S64201_V_AR", "AC Reactive Power", "", // ValuePoint.Type.INT16, true, AccessMode.READ_ONLY, Unit.VOLT_AMPERE_REACTIVE, "V_AR_SF")), // HZ(new ScaledValuePoint("S64201_HZ", "Line Frequency", "", // - ValuePoint.Type.INT16, true, AccessMode.READ_ONLY, Unit.MILLIHERTZ, "mHZ_SF")), // + ValuePoint.Type.UINT16, true, AccessMode.READ_ONLY, Unit.HERTZ, "Hz_SF") + ), // RESERVED_36(new ReservedPoint("S64201_RESERVED_36")), // RESERVED_37(new ReservedPoint("S64201_RESERVED_37")), // RESERVED_38(new ReservedPoint("S64201_RESERVED_38")), // @@ -271,6 +272,7 @@ public static enum S64201StVnd implements OptionsEnum { LINE_FAILURE_OVERVOLTAGE_3(46, "Line failure overvoltage L3 The voltage of a grid phase is too low; the grid cannot be fed into. The phase experiencing failure is displayed."), // GRID_FAILURE_PHASETOPHASE(47, "Grid failure phase-to-phase voltage"), // + LINE_FAILURE_UNDERFREQ(48, "Line failure: underfreq. Grid frequency is too low. This fault may be gridrelated."), // LINE_FAILURE_OVERFREQ(49, diff --git a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/statemachine/ErrorHandler.java b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/statemachine/ErrorHandler.java index 8dc242a57b6..1424879da21 100644 --- a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/statemachine/ErrorHandler.java +++ b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/statemachine/ErrorHandler.java @@ -14,11 +14,18 @@ protected void onEntry(Context context) throws OpenemsNamedException { } @Override - public State runAndGetNextState(Context context) throws OpenemsNamedException { + public State runAndGetNextState(Context context) { final var inverter = context.getParent(); if (!inverter.hasFailure()) { - return State.GO_STOPPED; + return State.UNDEFINED; } return State.ERROR; } + + @Override + protected void onExit(Context context) { + final var inverter = context.getParent(); + inverter._setMaxStartTimeout(false); + inverter._setMaxStopTimeout(false); + } } diff --git a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/test/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImplTest.java b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/test/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImplTest.java index 4bc316e97c5..fc68eda27ec 100644 --- a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/test/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImplTest.java +++ b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/test/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImplTest.java @@ -1,15 +1,18 @@ package io.openems.edge.batteryinverter.kaco.blueplanetgridsave; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; +import static io.openems.edge.batteryinverter.api.SymmetricBatteryInverter.ChannelId.MAX_APPARENT_POWER; +import static io.openems.edge.batteryinverter.kaco.blueplanetgridsave.BatteryInverterKacoBlueplanetGridsave.WATCHDOG_TIMEOUT_SECONDS; +import static io.openems.edge.batteryinverter.kaco.blueplanetgridsave.BatteryInverterKacoBlueplanetGridsave.WATCHDOG_TRIGGER_SECONDS; +import static io.openems.edge.batteryinverter.kaco.blueplanetgridsave.BatteryInverterKacoBlueplanetGridsave.ChannelId.STATE_MACHINE; +import static io.openems.edge.batteryinverter.kaco.blueplanetgridsave.KacoSunSpecModel.S64201.CURRENT_STATE; +import static io.openems.edge.batteryinverter.kaco.blueplanetgridsave.KacoSunSpecModel.S64201.WATCHDOG; +import static java.time.temporal.ChronoUnit.SECONDS; import org.junit.Before; import org.junit.Test; import io.openems.common.exceptions.OpenemsException; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.api.Battery; import io.openems.edge.battery.test.DummyBattery; import io.openems.edge.batteryinverter.kaco.blueplanetgridsave.KacoSunSpecModel.S64201.S64201CurrentState; @@ -24,25 +27,13 @@ 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.TestUtils; public class BatteryInverterKacoBlueplanetGridsaveImplTest { - private static final String BATTERY_INVERTER_ID = "batteryInverter0"; - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(BATTERY_INVERTER_ID, "StateMachine"); - - private static final ChannelAddress MAX_APPARENT_POWER = new ChannelAddress(BATTERY_INVERTER_ID, - "MaxApparentPower"); - private static final ChannelAddress CURRENT_STATE = new ChannelAddress(BATTERY_INVERTER_ID, - KacoSunSpecModel.S64201.CURRENT_STATE.getChannelId().id()); - private static final ChannelAddress WATCHDOG = new ChannelAddress(BATTERY_INVERTER_ID, - KacoSunSpecModel.S64201.WATCHDOG.getChannelId().id()); - private static class MyComponentTest extends ComponentTest { - private final Battery battery = new DummyBattery(BATTERY_ID); + private final Battery battery = new DummyBattery("battery0"); public MyComponentTest(OpenemsComponent sut) throws OpenemsException { super(sut); @@ -63,15 +54,13 @@ protected void handleEvent(String topic) throws Exception { @Before public void prepareTest() throws Exception { - final var start = 1577836800L; - clock = new TimeLeapClock(Instant.ofEpochSecond(start) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); + clock = TestUtils.createDummyClock(); var sut = new BatteryInverterKacoBlueplanetGridsaveImpl(); test = new MyComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)); + .addReference("setModbus", new DummyModbusBridge("modbus0")); // TODO implement proper Dummy-Modbus-Bridge with SunSpec support. Till then... test.addReference("isSunSpecInitializationCompleted", true); // @@ -86,16 +75,16 @@ public void prepareTest() throws Exception { addChannel.invoke(sut, KacoSunSpecModel.S64202.CHA_MAX_A_0.getChannelId()); addChannel.invoke(sut, KacoSunSpecModel.S64202.EN_LIMIT_0.getChannelId()); addChannel.invoke(sut, KacoSunSpecModel.S64201.REQUESTED_STATE.getChannelId()); - addChannel.invoke(sut, KacoSunSpecModel.S64201.CURRENT_STATE.getChannelId()); - addChannel.invoke(sut, KacoSunSpecModel.S64201.WATCHDOG.getChannelId()); + addChannel.invoke(sut, CURRENT_STATE.getChannelId()); + addChannel.invoke(sut, WATCHDOG.getChannelId()); addChannel.invoke(sut, KacoSunSpecModel.S64201.W_SET_PCT.getChannelId()); addChannel.invoke(sut, KacoSunSpecModel.S64201.WPARAM_RMP_TMS.getChannelId()); addChannel.invoke(sut, KacoSunSpecModel.S64201.ST_VND.getChannelId()); test.activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // + .setId("batteryInverter0") // .setStartStopConfig(StartStopConfig.START) // - .setModbusId(MODBUS_ID) // + .setModbusId("modbus0") // .setActivateWatchdog(true) // .build()); // } @@ -104,16 +93,16 @@ public void prepareTest() throws Exception { public void testStart() throws Exception { test // .next(new TestCase() // - .input(CURRENT_STATE, S64201CurrentState.STANDBY) // + .input(CURRENT_STATE.getChannelId(), S64201CurrentState.STANDBY) // .input(MAX_APPARENT_POWER, 50_000) // .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase() // - .timeleap(clock, 4, ChronoUnit.SECONDS) // + .timeleap(clock, 4, SECONDS) // .output(STATE_MACHINE, State.GO_RUNNING)) // .next(new TestCase() // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .input(CURRENT_STATE, S64201CurrentState.GRID_CONNECTED) // - .output(WATCHDOG, BatteryInverterKacoBlueplanetGridsave.WATCHDOG_TIMEOUT_SECONDS)) // + .timeleap(clock, 1, SECONDS) // + .input(CURRENT_STATE.getChannelId(), S64201CurrentState.GRID_CONNECTED) // + .output(WATCHDOG.getChannelId(), WATCHDOG_TIMEOUT_SECONDS)) // .next(new TestCase() // .output(STATE_MACHINE, State.RUNNING)) // ; @@ -123,14 +112,13 @@ public void testStart() throws Exception { public void testWatchdog() throws Exception { test // .next(new TestCase() // - .output(WATCHDOG, BatteryInverterKacoBlueplanetGridsave.WATCHDOG_TIMEOUT_SECONDS)) // + .output(WATCHDOG.getChannelId(), WATCHDOG_TIMEOUT_SECONDS)) // .next(new TestCase() // - .timeleap(clock, BatteryInverterKacoBlueplanetGridsave.WATCHDOG_TRIGGER_SECONDS - 1, - ChronoUnit.SECONDS) // - .output(WATCHDOG, null /* waiting till next watchdog trigger */)) // + .timeleap(clock, WATCHDOG_TRIGGER_SECONDS - 1, SECONDS) // + .output(WATCHDOG.getChannelId(), null /* waiting till next watchdog trigger */)) // .next(new TestCase() // - .timeleap(clock, 1, ChronoUnit.SECONDS) // - .output(WATCHDOG, BatteryInverterKacoBlueplanetGridsave.WATCHDOG_TIMEOUT_SECONDS)) // + .timeleap(clock, 1, SECONDS) // + .output(WATCHDOG.getChannelId(), WATCHDOG_TIMEOUT_SECONDS)) // ; } } diff --git a/io.openems.edge.batteryinverter.refu88k/src/io/openems/edge/batteryinverter/refu88k/BatteryInverterRefuStore88k.java b/io.openems.edge.batteryinverter.refu88k/src/io/openems/edge/batteryinverter/refu88k/BatteryInverterRefuStore88k.java index 2a9e69ceb53..64a0473325d 100644 --- a/io.openems.edge.batteryinverter.refu88k/src/io/openems/edge/batteryinverter/refu88k/BatteryInverterRefuStore88k.java +++ b/io.openems.edge.batteryinverter.refu88k/src/io/openems/edge/batteryinverter/refu88k/BatteryInverterRefuStore88k.java @@ -43,16 +43,22 @@ public interface BatteryInverterRefuStore88k */ public static int RETRY_COMMAND_MAX_ATTEMPTS = 30; + /* + * Whenever one of these states would be Level.FAULT, the EssGeneric will stop + * the battery and the inverter. If this is necessary, it must be specifically + * mentioned and the state should have a proper description of the fault. + */ + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { STATE_MACHINE(Doc.of(State.values()) // .text("Current State of State-Machine")), // - RUN_FAILED(Doc.of(Level.FAULT) // + RUN_FAILED(Doc.of(Level.WARNING) // .text("Running the Logic failed")), // - MAX_START_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_START_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of start attempts failed")), // - MAX_STOP_ATTEMPTS(Doc.of(Level.FAULT) // + MAX_STOP_ATTEMPTS(Doc.of(Level.WARNING) // .text("The maximum number of stop attempts failed")), // - INVERTER_CURRENT_STATE_FAULT(Doc.of(Level.FAULT) // + INVERTER_CURRENT_STATE_FAULT(Doc.of(Level.WARNING) // .text("The 'CurrentState' is invalid")), // /* @@ -106,41 +112,41 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { ST(Doc.of(OperatingState.values())), // ST_VND(Doc.of(VendorOperatingState.values())), // // Evt1 Alarms and Warnings - GROUND_FAULT(Doc.of(Level.FAULT) // + GROUND_FAULT(Doc.of(Level.WARNING) // .text("Ground fault")), // - DC_OVER_VOLTAGE(Doc.of(Level.FAULT) // + DC_OVER_VOLTAGE(Doc.of(Level.WARNING) // .text("Dc over voltage")), // - AC_DISCONNECT(Doc.of(Level.FAULT) // + AC_DISCONNECT(Doc.of(Level.WARNING) // .text("AC disconnect open")), // - DC_DISCONNECT(Doc.of(Level.FAULT) // + DC_DISCONNECT(Doc.of(Level.WARNING) // .text("DC disconnect open")), // - GRID_DISCONNECT(Doc.of(Level.FAULT) // + GRID_DISCONNECT(Doc.of(Level.WARNING) // .text("Grid shutdown")), // - CABINET_OPEN(Doc.of(Level.FAULT) // + CABINET_OPEN(Doc.of(Level.WARNING) // .text("Cabinet open")), // - MANUAL_SHUTDOWN(Doc.of(Level.FAULT) // + MANUAL_SHUTDOWN(Doc.of(Level.WARNING) // .text("Manual shutdown")), // - OVER_TEMP(Doc.of(Level.FAULT) // + OVER_TEMP(Doc.of(Level.WARNING) // .text("Over temperature")), // - OVER_FREQUENCY(Doc.of(Level.FAULT) // + OVER_FREQUENCY(Doc.of(Level.WARNING) // .text("Frequency above limit")), // - UNDER_FREQUENCY(Doc.of(Level.FAULT) // + UNDER_FREQUENCY(Doc.of(Level.WARNING) // .text("Frequency under limit")), // - AC_OVER_VOLT(Doc.of(Level.FAULT) // + AC_OVER_VOLT(Doc.of(Level.WARNING) // .text("AC Voltage above limit")), // - AC_UNDER_VOLT(Doc.of(Level.FAULT) // + AC_UNDER_VOLT(Doc.of(Level.WARNING) // .text("AC Voltage under limit")), // - BLOWN_STRING_FUSE(Doc.of(Level.FAULT) // + BLOWN_STRING_FUSE(Doc.of(Level.WARNING) // .text("Blown String fuse on input")), // - UNDER_TEMP(Doc.of(Level.FAULT) // + UNDER_TEMP(Doc.of(Level.WARNING) // .text("Under temperature")), // - MEMORY_LOSS(Doc.of(Level.FAULT) // + MEMORY_LOSS(Doc.of(Level.WARNING) // .text("Generic Memory or Communication error (internal)")), // - HW_TEST_FAILURE(Doc.of(Level.FAULT) // + HW_TEST_FAILURE(Doc.of(Level.WARNING) // .text("Hardware test failure")), // - OTHER_ALARM(Doc.of(Level.FAULT) // + OTHER_ALARM(Doc.of(Level.WARNING) // .text("Other alarm")), // - OTHER_WARNING(Doc.of(Level.FAULT) // + OTHER_WARNING(Doc.of(Level.WARNING) // .text("Other warning")), // EVT_2(Doc.of(OpenemsType.INTEGER).unit(Unit.NONE).accessMode(AccessMode.READ_ONLY)), // EVT_VND_1(Doc.of(OpenemsType.INTEGER).unit(Unit.NONE).accessMode(AccessMode.READ_ONLY)), // diff --git a/io.openems.edge.batteryinverter.refu88k/test/io/openems/edge/batteryinverter/refu88k/BatteryInverterRefuStore88kImplTest.java b/io.openems.edge.batteryinverter.refu88k/test/io/openems/edge/batteryinverter/refu88k/BatteryInverterRefuStore88kImplTest.java index 08316f856cf..560aa3bb4f3 100644 --- a/io.openems.edge.batteryinverter.refu88k/test/io/openems/edge/batteryinverter/refu88k/BatteryInverterRefuStore88kImplTest.java +++ b/io.openems.edge.batteryinverter.refu88k/test/io/openems/edge/batteryinverter/refu88k/BatteryInverterRefuStore88kImplTest.java @@ -9,17 +9,14 @@ public class BatteryInverterRefuStore88kImplTest { - private static final String BATTERY_INVERTER_ID = "batteryInverter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new BatteryInverterRefuStore88kImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // .setStartStop(StartStopConfig.AUTO) // .setTimeLimitNoPower(0) // .setWatchdoginterval(0) // diff --git a/io.openems.edge.batteryinverter.sinexcel/src/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcel.java b/io.openems.edge.batteryinverter.sinexcel/src/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcel.java index daa20ac5029..d54e8bbe2d1 100644 --- a/io.openems.edge.batteryinverter.sinexcel/src/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcel.java +++ b/io.openems.edge.batteryinverter.sinexcel/src/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcel.java @@ -44,7 +44,7 @@ public interface BatteryInverterSinexcel extends OffGridBatteryInverter, Managed public enum ChannelId implements io.openems.edge.common.channel.ChannelId { STATE_MACHINE(Doc.of(State.values()) // .text("Current State of State-Machine")), // - RUN_FAILED(Doc.of(Level.FAULT) // + RUN_FAILED(Doc.of(Level.WARNING) // .text("Running the Logic failed")), // SET_ACTIVE_POWER(Doc.of(OpenemsType.INTEGER) // .accessMode(AccessMode.READ_WRITE)// @@ -96,7 +96,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { SERIAL_NUMBER(Doc.of(OpenemsType.STRING) // .persistencePriority(PersistencePriority.HIGH) // .accessMode(AccessMode.READ_ONLY)), // - FAULT_STATUS(Doc.of(Level.FAULT) // + FAULT_STATUS(Doc.of(Level.WARNING) // .accessMode(AccessMode.READ_ONLY)), // ALERT_STATUS(Doc.of(Level.WARNING) // .accessMode(AccessMode.READ_ONLY)), // diff --git a/io.openems.edge.batteryinverter.sinexcel/test/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcelImplTest.java b/io.openems.edge.batteryinverter.sinexcel/test/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcelImplTest.java index 6092ebde7d6..abf144c927e 100644 --- a/io.openems.edge.batteryinverter.sinexcel/test/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcelImplTest.java +++ b/io.openems.edge.batteryinverter.sinexcel/test/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcelImplTest.java @@ -1,5 +1,11 @@ package io.openems.edge.batteryinverter.sinexcel; +import static io.openems.edge.batteryinverter.api.OffGridBatteryInverter.ChannelId.INVERTER_STATE; +import static io.openems.edge.batteryinverter.api.SymmetricBatteryInverter.ChannelId.MAX_APPARENT_POWER; +import static io.openems.edge.batteryinverter.sinexcel.BatteryInverterSinexcel.ChannelId.SET_OFF_GRID_MODE; +import static io.openems.edge.batteryinverter.sinexcel.BatteryInverterSinexcel.ChannelId.SET_ON_GRID_MODE; +import static io.openems.edge.batteryinverter.sinexcel.BatteryInverterSinexcel.ChannelId.STATE_MACHINE; + import java.time.Instant; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; @@ -8,10 +14,8 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.api.Battery; import io.openems.edge.battery.test.DummyBattery; -import io.openems.edge.batteryinverter.api.OffGridBatteryInverter; import io.openems.edge.batteryinverter.api.OffGridBatteryInverter.TargetGridMode; import io.openems.edge.batteryinverter.sinexcel.enums.CountryCode; import io.openems.edge.batteryinverter.sinexcel.enums.EnableDisable; @@ -27,21 +31,9 @@ public class BatteryInverterSinexcelImplTest { - private static final String BATTERY_INVERTER_ID = "batteryInverter0"; - private static final String BATTERY_ID = "battery0"; - private static final String MODBUS_ID = "modbus0"; - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(BATTERY_INVERTER_ID, "StateMachine"); - private static final ChannelAddress SET_ON_GRID_MODE = new ChannelAddress(BATTERY_INVERTER_ID, "SetOnGridMode"); - private static final ChannelAddress SET_OFF_GRID_MODE = new ChannelAddress(BATTERY_INVERTER_ID, "SetOffGridMode"); - private static final ChannelAddress MAX_APPARENT_POWER = new ChannelAddress(BATTERY_INVERTER_ID, // - "MaxApparentPower"); - - private static final ChannelAddress INVERTER_STATE = new ChannelAddress(BATTERY_INVERTER_ID, // - OffGridBatteryInverter.ChannelId.INVERTER_STATE.id()); - private static class MyComponentTest extends ComponentTest { - private final Battery battery = new DummyBattery(BATTERY_ID); + private final Battery battery = new DummyBattery("battery0"); public MyComponentTest(OpenemsComponent sut) throws OpenemsException { super(sut); @@ -64,11 +56,11 @@ public void testStart() throws Exception { new MyComponentTest(new BatteryInverterSinexcelImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // + .setId("batteryInverter0") // .setStartStopConfig(StartStopConfig.START) // - .setModbusId(MODBUS_ID) // + .setModbusId("modbus0") // .setCountryCode(CountryCode.GERMANY)// .setEmergencyPower(EnableDisable.DISABLE)// .build()) // @@ -100,11 +92,11 @@ public void testOffGrid() throws Exception { new MyComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // + .setId("batteryInverter0") // .setStartStopConfig(StartStopConfig.START) // - .setModbusId(MODBUS_ID) // + .setModbusId("modbus0") // .setCountryCode(CountryCode.GERMANY)// .setEmergencyPower(EnableDisable.DISABLE)// .build()) // diff --git a/io.openems.edge.bosch.bpts5hybrid/src/io/openems/edge/bosch/bpts5hybrid/meter/BoschBpts5HybridMeterImpl.java b/io.openems.edge.bosch.bpts5hybrid/src/io/openems/edge/bosch/bpts5hybrid/meter/BoschBpts5HybridMeterImpl.java index 74c10e22aad..03f72961afa 100644 --- a/io.openems.edge.bosch.bpts5hybrid/src/io/openems/edge/bosch/bpts5hybrid/meter/BoschBpts5HybridMeterImpl.java +++ b/io.openems.edge.bosch.bpts5hybrid/src/io/openems/edge/bosch/bpts5hybrid/meter/BoschBpts5HybridMeterImpl.java @@ -17,13 +17,13 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.MeterType; import io.openems.edge.bosch.bpts5hybrid.core.BoschBpts5HybridCore; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/core/BoschBpts5HybridCoreImplTest.java b/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/core/BoschBpts5HybridCoreImplTest.java index b7f597fbc35..e13cd08a7ab 100644 --- a/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/core/BoschBpts5HybridCoreImplTest.java +++ b/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/core/BoschBpts5HybridCoreImplTest.java @@ -2,6 +2,7 @@ import org.junit.Test; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -21,6 +22,7 @@ public void test() throws Exception { .setIpaddress("127.0.0.1") // .setInterval(2) // .build()) // - ; + .next(new TestCase()) // + .deactivate(); } } \ No newline at end of file diff --git a/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/ess/BoschBpts5HybridEssTest.java b/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/ess/BoschBpts5HybridEssTest.java index c45058b22e5..0a2d9d77811 100644 --- a/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/ess/BoschBpts5HybridEssTest.java +++ b/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/ess/BoschBpts5HybridEssTest.java @@ -3,6 +3,7 @@ import org.junit.Test; import io.openems.edge.bosch.bpts5hybrid.core.BoschBpts5HybridCoreImpl; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -30,6 +31,7 @@ public void test() throws Exception { .setId(ESS_ID) // .setCoreId(CORE_ID) // .build()) // - ; + .next(new TestCase()) // + .deactivate(); } } \ No newline at end of file diff --git a/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/meter/BoschBpts5HybridMeterTest.java b/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/meter/BoschBpts5HybridMeterTest.java index 476489b3ef7..1d3af580ac8 100644 --- a/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/meter/BoschBpts5HybridMeterTest.java +++ b/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/meter/BoschBpts5HybridMeterTest.java @@ -3,6 +3,7 @@ import org.junit.Test; import io.openems.edge.bosch.bpts5hybrid.core.BoschBpts5HybridCoreImpl; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -30,6 +31,7 @@ public void test() throws Exception { .setId(METER_ID) // .setCoreId(CORE_ID) // .build()) // - ; + .next(new TestCase()) // + .deactivate(); } } \ No newline at end of file diff --git a/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/pv/BoschBpts5HybridPvTest.java b/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/pv/BoschBpts5HybridPvTest.java index 5d0041227f6..8a411cf25e0 100644 --- a/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/pv/BoschBpts5HybridPvTest.java +++ b/io.openems.edge.bosch.bpts5hybrid/test/io/openems/edge/bosch/bpts5hybrid/pv/BoschBpts5HybridPvTest.java @@ -3,6 +3,7 @@ import org.junit.Test; import io.openems.edge.bosch.bpts5hybrid.core.BoschBpts5HybridCoreImpl; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -30,6 +31,7 @@ public void test() throws Exception { .setId(CHARGER_ID) // .setCoreId(CORE_ID) // .build()) // - ; + .next(new TestCase()) // + .deactivate(); } } \ No newline at end of file diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/UrlBuilder.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/UrlBuilder.java new file mode 100644 index 00000000000..095b1fe2c24 --- /dev/null +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/UrlBuilder.java @@ -0,0 +1,246 @@ +package io.openems.edge.bridge.http.api; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toUnmodifiableMap; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Simple URL Builder class to build URLs with correctly encoded query + * parameter. This class is immutable. + * + *

+ * Example Usage: + * + *

+ * 
+ * final var url = UrlBuilder.create() //
+		.withScheme("https") //
+		.withHost("openems.io") //
+		.withPort(443) //
+		.withPath("/path/to") //
+		.withQueryParam("key", "value") //
+		.withFragment("fragment") //
+		// get final result
+		.toUri() // to get a URI Object
+		.toUrl() // to get a URL Object
+		.toEncodedString() // to get the URL as a encoded String
+ * 
+ * or parse from a string
+ * 
+ * final var url = UrlBuilder.from("https://openems.io:443/path?key=value#fragment");
+ * 
+ * 
+ * + *

+ * URL-Schema: + * scheme://host:port/path?queryParams#fragment + * + */ +public final class UrlBuilder { + + /** + * Parses a raw uri string to the url parts. + * + * @param uriString the raw string to parse + * @return the {@link UrlBuilder} with the parts of th uri + */ + public static UrlBuilder parse(String uriString) { + final var uri = URI.create(uriString); + + final var query = uri.getQuery(); + final var queryParams = query == null ? Collections.emptyMap() + : Stream.of(uri.getQuery().split("&")) // + .map(t -> t.split("=")) // + .collect(toUnmodifiableMap(t -> t[0], t -> t[1])); + return new UrlBuilder(// + uri.getScheme(), // + uri.getHost(), // + uri.getPort(), // + uri.getPath(), // + queryParams, // + uri.getFragment() // + ); + } + + /** + * Creates a new {@link UrlBuilder}. + * + * @return the new {@link UrlBuilder} instance + */ + public static UrlBuilder create() { + return new UrlBuilder(null, null, null, null, Collections.emptyMap(), null); + } + + private final String scheme; + private final String host; + private final Integer port; + private final String path; + private final Map queryParams; + private final String fragment; + + private UrlBuilder(// + String scheme, // + String host, // + Integer port, // + String path, // + Map queryParams, // + String fragment // + ) { + this.scheme = scheme; + this.host = host; + this.port = port; + this.path = path; + this.queryParams = queryParams; + this.fragment = fragment; + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new scheme. + * + * @param scheme the new scheme + * @return the copy of the {@link UrlBuilder} with the new scheme + */ + public UrlBuilder withScheme(String scheme) { + return new UrlBuilder(scheme, this.host, this.port, this.path, this.queryParams, this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new host. + * + * @param host the new host + * @return the copy of the {@link UrlBuilder} with the new host + */ + public UrlBuilder withHost(String host) { + return new UrlBuilder(this.scheme, host, this.port, this.path, this.queryParams, this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new port. + * + * @param port the new port + * @return the copy of the {@link UrlBuilder} with the new port + */ + public UrlBuilder withPort(int port) { + if (port < 0) { + throw new IllegalArgumentException("Property 'port' must not be smaller than '0'."); + } + return new UrlBuilder(this.scheme, this.host, port, this.path, this.queryParams, this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new path. + * + * @param path the new path + * @return the copy of the {@link UrlBuilder} with the new path + */ + public UrlBuilder withPath(String path) { + return new UrlBuilder(this.scheme, this.host, this.port, path, this.queryParams, this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new query parameter + * added. + * + * @param key the key of the new query parameter + * @param value the value of the new query parameter + * @return the copy of the {@link UrlBuilder} with the new query parameter added + */ + public UrlBuilder withQueryParam(String key, String value) { + Map newQueryParams = new HashMap<>(this.queryParams); + newQueryParams.put(key, value); + return new UrlBuilder(this.scheme, this.host, this.port, this.path, Collections.unmodifiableMap(newQueryParams), + this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new fragment. + * + * @param fragment the new fragment + * @return the copy of the {@link UrlBuilder} with the new fragment + */ + public UrlBuilder withFragment(String fragment) { + return new UrlBuilder(this.scheme, this.host, this.port, this.path, this.queryParams, fragment); + } + + /** + * Creates a {@link URI} from this object. + * + * @return the {@link URI} + */ + public URI toUri() { + return URI.create(this.toEncodedString()); + } + + /** + * Creates a {@link URI} from this object. + * + * @return the {@link URI} + * @throws MalformedURLException If a protocol handler for the URL could not be + * found, or if some other error occurred while + * constructing the URL + */ + public URL toUrl() throws MalformedURLException { + return this.toUri().toURL(); + } + + /** + * Creates an encoded string url from this object. + * + *

+ * Note: does not check if the url is valid. To Check if it is valid use + * {@link #toUrl()} + * + * @return the encoded url + */ + public String toEncodedString() { + final var url = new StringBuilder(); + + url.append(this.scheme); + url.append("://"); + url.append(this.host); + + if (this.port != null) { + url.append(":"); + url.append(this.port); + } + + if (this.path != null && !this.path.isEmpty()) { + if (!this.path.startsWith("/")) { + url.append("/"); + } + url.append(this.path); + } + + if (!this.queryParams.isEmpty()) { + var query = this.queryParams.entrySet().stream() // + .map(t -> encode(t.getKey()) + "=" + encode(t.getValue())) // + .collect(joining("&", "?", "")); + url.append(query); + } + + if (this.fragment != null && !this.fragment.isEmpty()) { + if (!this.fragment.startsWith("#")) { + url.append("#"); + } + url.append(this.fragment); + } + + return url.toString(); + } + + // Helper method to URL-encode values + private static String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8) // + .replace("+", "%20") // " " => "+" => "%20" + ; + } +} diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpBundle.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpBundle.java index befb0e1ebca..fcd0d24b56d 100644 --- a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpBundle.java +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpBundle.java @@ -1,5 +1,6 @@ package io.openems.edge.bridge.http.dummy; +import static io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory.dummyEndpointFetcher; import static java.util.Collections.emptyMap; import java.util.concurrent.CompletableFuture; @@ -7,7 +8,6 @@ import org.osgi.service.event.Event; -import io.openems.common.test.TimeLeapClock; import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; import io.openems.edge.bridge.http.api.BridgeHttpFactory; import io.openems.edge.bridge.http.api.CycleSubscriber; @@ -17,9 +17,8 @@ public class DummyBridgeHttpBundle { - private final DummyEndpointFetcher fetcher = DummyBridgeHttpFactory.dummyEndpointFetcher(); - private final DummyBridgeHttpExecutor pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(new TimeLeapClock(), - true); + private final DummyEndpointFetcher fetcher = dummyEndpointFetcher(); + private final DummyBridgeHttpExecutor pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(true); private final CycleSubscriber cycleSubscriber = DummyBridgeHttpFactory.cycleSubscriber(); private final DummyBridgeHttpFactory bridgeFactory = DummyBridgeHttpFactory.ofBridgeImpl(() -> this.cycleSubscriber, () -> this.fetcher, () -> this.pool); diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpExecutor.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpExecutor.java index 2a74f1c7ea0..cf1d07df092 100644 --- a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpExecutor.java +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpExecutor.java @@ -1,5 +1,7 @@ package io.openems.edge.bridge.http.dummy; +import static io.openems.edge.common.test.TestUtils.createDummyClock; + import java.time.Clock; import java.time.Duration; import java.time.Instant; @@ -110,10 +112,18 @@ public DummyBridgeHttpExecutor(Clock clock, boolean handleTasksImmediately) { this.taskExecutor = handleTasksImmediately ? new ImmediateTaskExecutor() : new DelayedTaskExecutor(); } + public DummyBridgeHttpExecutor(boolean handleTasksImmediately) { + this(createDummyClock(), handleTasksImmediately); + } + public DummyBridgeHttpExecutor(Clock clock) { this(clock, false); } + public DummyBridgeHttpExecutor() { + this(false); + } + @Override public ScheduledFuture schedule(Runnable task, Delay.DurationDelay durationDelay) { if (this.isShutdown()) { diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpFactory.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpFactory.java index 635b56ad885..01f1f169e83 100644 --- a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpFactory.java +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttpFactory.java @@ -124,6 +124,21 @@ public static DummyBridgeHttpExecutor dummyBridgeHttpExecutor(// return new DummyBridgeHttpExecutor(clock, handleTasksImmediately); } + /** + * Creates a {@link DummyBridgeHttpExecutor} to handle the execution of the + * requests to fetch an {@link Endpoint}. + * + * @param handleTasksImmediately true if all tasks which are not scheduled + * should be executed immediately in the same + * thread; false if only executed during the + * {@link DummyBridgeHttpExecutor#update()} + * method. + * @return the created {@link DummyBridgeHttpExecutor} + */ + public static DummyBridgeHttpExecutor dummyBridgeHttpExecutor(boolean handleTasksImmediately) { + return new DummyBridgeHttpExecutor(handleTasksImmediately); + } + /** * Creates a {@link DummyBridgeHttpExecutor} to handle the execution of the * requests to fetch an {@link Endpoint}. @@ -136,6 +151,16 @@ public static DummyBridgeHttpExecutor dummyBridgeHttpExecutor(Clock clock) { return new DummyBridgeHttpExecutor(clock); } + /** + * Creates a {@link DummyBridgeHttpExecutor} to handle the execution of the + * requests to fetch an {@link Endpoint}. + * + * @return the created {@link DummyBridgeHttpExecutor} + */ + public static DummyBridgeHttpExecutor dummyBridgeHttpExecutor() { + return new DummyBridgeHttpExecutor(); + } + private DummyBridgeHttpFactory(Supplier supplier) { super(new DummyBridgeHttpCso(supplier)); } diff --git a/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpCycleTest.java b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpCycleTest.java index 0e4ca875adc..ca2a7d82130 100644 --- a/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpCycleTest.java +++ b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpCycleTest.java @@ -13,7 +13,6 @@ import org.junit.Test; import org.osgi.service.event.Event; -import io.openems.common.test.TimeLeapClock; import io.openems.common.utils.FunctionUtils; import io.openems.edge.bridge.http.BridgeHttpImpl; import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; @@ -33,7 +32,7 @@ public class BridgeHttpCycleTest { public void before() throws Exception { this.cycleSubscriber = new CycleSubscriber(); this.fetcher = new DummyEndpointFetcher(); - this.pool = new DummyBridgeHttpExecutor(new TimeLeapClock()); + this.pool = new DummyBridgeHttpExecutor(); this.bridgeHttp = new BridgeHttpImpl(// this.cycleSubscriber, // this.fetcher, // diff --git a/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpTest.java b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpTest.java index 013e2ddc067..253668672e1 100644 --- a/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpTest.java +++ b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpTest.java @@ -1,5 +1,6 @@ package io.openems.edge.bridge.http.api; +import static io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory.dummyBridgeHttpExecutor; import static java.util.Collections.emptyMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -14,7 +15,6 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.function.ThrowingFunction; -import io.openems.common.test.TimeLeapClock; import io.openems.common.utils.JsonUtils; import io.openems.edge.bridge.http.BridgeHttpImpl; import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; @@ -31,8 +31,7 @@ public class BridgeHttpTest { public void before() throws Exception { this.cycleSubscriber = DummyBridgeHttpFactory.cycleSubscriber(); this.fetcher = DummyBridgeHttpFactory.dummyEndpointFetcher(); - this.bridgeHttp = new BridgeHttpImpl(this.cycleSubscriber, this.fetcher, - DummyBridgeHttpFactory.dummyBridgeHttpExecutor(new TimeLeapClock(), true)); + this.bridgeHttp = new BridgeHttpImpl(this.cycleSubscriber, this.fetcher, dummyBridgeHttpExecutor(true)); } @After diff --git a/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpTimeTest.java b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpTimeTest.java index 74f89ade961..db098dab702 100644 --- a/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpTimeTest.java +++ b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/BridgeHttpTimeTest.java @@ -1,6 +1,7 @@ package io.openems.edge.bridge.http.api; import static io.openems.edge.bridge.http.time.DelayTimeProviderChain.fixedDelay; +import static io.openems.edge.common.test.TestUtils.createDummyClock; import static org.junit.Assert.assertEquals; import java.time.Duration; @@ -35,7 +36,7 @@ public void before() throws Exception { }; }); - this.pool = new DummyBridgeHttpExecutor(this.clock = new TimeLeapClock()); + this.pool = new DummyBridgeHttpExecutor(this.clock = createDummyClock()); this.bridgeHttp = new BridgeHttpImpl(cycleSubscriber, fetcher, this.pool); } diff --git a/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/UrlBuilderTest.java b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/UrlBuilderTest.java new file mode 100644 index 00000000000..7bfbc193476 --- /dev/null +++ b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/UrlBuilderTest.java @@ -0,0 +1,117 @@ +package io.openems.edge.bridge.http.api; + +import static org.junit.Assert.assertEquals; + +import java.net.URI; + +import org.junit.Test; + +public class UrlBuilderTest { + + @Test + public void testParse() { + final var rawUrl = "https://openems.io:443/path?key=value#fragment"; + final var parsedUrl = UrlBuilder.parse(rawUrl); + assertEquals(rawUrl, parsedUrl.toEncodedString()); + } + + @Test + public void testParseNoQueryParams() { + final var rawUrl = "https://openems.io:443/path#fragment"; + final var parsedUrl = UrlBuilder.parse(rawUrl); + assertEquals(rawUrl, parsedUrl.toEncodedString()); + } + + @Test + public void testScheme() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io"); + + assertEquals("https://openems.io", url.toEncodedString()); + assertEquals("http://openems.io", url.withScheme("http").toEncodedString()); + } + + @Test + public void testHost() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io"); + + assertEquals("https://openems.io", url.toEncodedString()); + assertEquals("https://better.openems.io", url.withHost("better.openems.io").toEncodedString()); + } + + @Test + public void testPort() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withPort(443); + + assertEquals("https://openems.io:443", url.toEncodedString()); + assertEquals("https://openems.io:445", url.withPort(445).toEncodedString()); + } + + @Test + public void testPath() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withPath("/path"); + + assertEquals("https://openems.io/path", url.toEncodedString()); + assertEquals("https://openems.io/path/abc", url.withPath("/path/abc").toEncodedString()); + assertEquals("https://openems.io/withoutslash", url.withPath("withoutslash").toEncodedString()); + } + + @Test + public void testQueryParameter() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withQueryParam("key", "value"); + + assertEquals("https://openems.io?key=value", url.toEncodedString()); + assertEquals("https://openems.io?key=otherValue", url.withQueryParam("key", "otherValue").toEncodedString()); + } + + @Test + public void testFragment() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withFragment("myFragment"); + + assertEquals("https://openems.io#myFragment", url.toEncodedString()); + assertEquals("https://openems.io#myOtherFragment", url.withFragment("myOtherFragment").toEncodedString()); + assertEquals("https://openems.io#with", url.withFragment("#with").toEncodedString()); + } + + @Test + public void testToUri() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withPort(443) // + .withPath("/path") // + .withQueryParam("key", "value") // + .withFragment("fragment"); + + assertEquals(URI.create("https://openems.io:443/path?key=value#fragment"), url.toUri()); + } + + @Test + public void testToEncodedString() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withPort(443) // + .withPath("/path") // + .withQueryParam("key", "va lu+e") // + .withFragment("fragment"); + + assertEquals("https://openems.io:443/path?key=va%20lu%2Be#fragment", url.toEncodedString()); + } + +} diff --git a/io.openems.edge.bridge.mbus/test/io/openems/edge/bridge/mbus/BridgeMbusImplTest.java b/io.openems.edge.bridge.mbus/test/io/openems/edge/bridge/mbus/BridgeMbusImplTest.java index 799f89f4971..79e1f813886 100644 --- a/io.openems.edge.bridge.mbus/test/io/openems/edge/bridge/mbus/BridgeMbusImplTest.java +++ b/io.openems.edge.bridge.mbus/test/io/openems/edge/bridge/mbus/BridgeMbusImplTest.java @@ -2,22 +2,18 @@ import org.junit.Test; -import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; public class BridgeMbusImplTest { - private static final String COMPONENT_ID = "mbus0"; - @Test public void test() throws Exception { new ComponentTest(new BridgeMbusImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("mbus0") // .setPortName("/dev/ttyUSB0") // .setBaudrate(2400) // - .build()) // - .next(new TestCase()); // + .build()); // } } diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/BridgeModbusSerialImpl.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/BridgeModbusSerialImpl.java index d36835f41f8..3c5c5d64c19 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/BridgeModbusSerialImpl.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/BridgeModbusSerialImpl.java @@ -24,6 +24,7 @@ import io.openems.edge.bridge.modbus.api.AbstractModbusBridge; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.BridgeModbusSerial; +import io.openems.edge.bridge.modbus.api.Config; import io.openems.edge.bridge.modbus.api.Parity; import io.openems.edge.bridge.modbus.api.Stopbit; import io.openems.edge.common.component.OpenemsComponent; @@ -86,15 +87,15 @@ public BridgeModbusSerialImpl() { @Activate private void activate(ComponentContext context, ConfigSerial config) { - super.activate(context, config.id(), config.alias(), config.enabled(), config.logVerbosity(), - config.invalidateElementsAfterReadErrors()); + super.activate(context, new Config(config.id(), config.alias(), config.enabled(), config.logVerbosity(), + config.invalidateElementsAfterReadErrors())); this.applyConfig(config); } @Modified private void modified(ComponentContext context, ConfigSerial config) { - super.modified(context, config.id(), config.alias(), config.enabled(), config.logVerbosity(), - config.invalidateElementsAfterReadErrors()); + super.modified(context, new Config(config.id(), config.alias(), config.enabled(), config.logVerbosity(), + config.invalidateElementsAfterReadErrors())); this.applyConfig(config); this.closeModbusConnection(); } diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/BridgeModbusTcpImpl.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/BridgeModbusTcpImpl.java index 9cd9cd3dff3..aacd72d89fd 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/BridgeModbusTcpImpl.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/BridgeModbusTcpImpl.java @@ -22,6 +22,7 @@ import io.openems.edge.bridge.modbus.api.AbstractModbusBridge; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.BridgeModbusTcp; +import io.openems.edge.bridge.modbus.api.Config; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; @@ -56,15 +57,15 @@ public BridgeModbusTcpImpl() { @Activate private void activate(ComponentContext context, ConfigTcp config) throws UnknownHostException { - super.activate(context, config.id(), config.alias(), config.enabled(), config.logVerbosity(), - config.invalidateElementsAfterReadErrors()); + super.activate(context, new Config(config.id(), config.alias(), config.enabled(), config.logVerbosity(), + config.invalidateElementsAfterReadErrors())); this.applyConfig(config); } @Modified private void modified(ComponentContext context, ConfigTcp config) throws UnknownHostException { - super.modified(context, config.id(), config.alias(), config.enabled(), config.logVerbosity(), - config.invalidateElementsAfterReadErrors()); + super.modified(context, new Config(config.id(), config.alias(), config.enabled(), config.logVerbosity(), + config.invalidateElementsAfterReadErrors())); this.applyConfig(config); this.closeModbusConnection(); } diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/AbstractModbusBridge.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/AbstractModbusBridge.java index 52087943576..2410fbaf055 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/AbstractModbusBridge.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/AbstractModbusBridge.java @@ -1,6 +1,5 @@ package io.openems.edge.bridge.modbus.api; -import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.osgi.service.component.ComponentContext; @@ -35,8 +34,7 @@ public abstract class AbstractModbusBridge extends AbstractOpenemsComponent impl */ protected static final int DEFAULT_RETRIES = 1; - private final AtomicReference logVerbosity = new AtomicReference<>(LogVerbosity.NONE); - private int invalidateElementsAfterReadErrors = 1; + private Config config = null; protected final ModbusWorker worker = new ModbusWorker( // Execute Task @@ -47,8 +45,8 @@ public abstract class AbstractModbusBridge extends AbstractOpenemsComponent impl state -> this._setCycleTimeIsTooShort(state), // Set ChannelId.CYCLE_DELAY cycleDelay -> this._setCycleDelay(cycleDelay), - // LogVerbosity - this.logVerbosity // + // LogHandler + () -> this.config.log // ); protected AbstractModbusBridge(io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, @@ -62,12 +60,11 @@ protected void activate(ComponentContext context, String id, String alias, boole throw new IllegalArgumentException("Use the other activate() method."); } - protected void activate(ComponentContext context, String id, String alias, boolean enabled, - LogVerbosity logVerbosity, int invalidateElementsAfterReadErrors) { - super.activate(context, id, alias, enabled); - this.applyConfig(logVerbosity, invalidateElementsAfterReadErrors); - if (enabled) { - this.worker.activate(id); + protected void activate(ComponentContext context, Config config) { + super.activate(context, config.id, config.alias, config.enabled); + this.applyConfig(config); + if (config.enabled) { + this.worker.activate(config.id); } } @@ -84,20 +81,18 @@ protected void modified(ComponentContext context, String id, String alias, boole throw new IllegalArgumentException("Use the other modified() method."); } - protected void modified(ComponentContext context, String id, String alias, boolean enabled, - LogVerbosity logVerbosity, int invalidateElementsAfterReadErrors) { - super.modified(context, id, alias, enabled); - this.applyConfig(logVerbosity, invalidateElementsAfterReadErrors); - if (enabled) { - this.worker.modified(id); + protected void modified(ComponentContext context, Config config) { + super.modified(context, config.id, config.alias, config.enabled); + this.applyConfig(config); + if (config.enabled) { + this.worker.modified(config.id); } else { this.worker.deactivate(); } } - private void applyConfig(LogVerbosity logVerbosity, int invalidateElementsAfterReadErrors) { - this.logVerbosity.set(logVerbosity); - this.invalidateElementsAfterReadErrors = invalidateElementsAfterReadErrors; + private void applyConfig(Config config) { + this.config = config; } /** @@ -124,22 +119,24 @@ public void removeProtocol(String sourceId) { @Override public void handleEvent(Event event) { - if (!this.isEnabled()) { + if (this.config == null || !this.isEnabled()) { return; } switch (event.getTopic()) { - case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: - this.worker.onBeforeProcessImage(); - break; - case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE: - this.worker.onExecuteWrite(); - break; + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // + -> this.worker.onBeforeProcessImage(); + + case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE // + -> this.worker.onExecuteWrite(); } } @Override public String debugLog() { - return switch (this.logVerbosity.get()) { + if (this.config == null) { + return null; + } + return switch (this.config.log.verbosity) { case NONE -> // null; case DEBUG_LOG, READS_AND_WRITES, READS_AND_WRITES_DURATION, READS_AND_WRITES_VERBOSE, @@ -167,7 +164,7 @@ public String debugLog() { * @return {@link LogVerbosity} */ public LogVerbosity getLogVerbosity() { - return this.logVerbosity.get(); + return this.config.log.verbosity; } /** @@ -177,7 +174,7 @@ public LogVerbosity getLogVerbosity() { * @return value */ public int invalidateElementsAfterReadErrors() { - return this.invalidateElementsAfterReadErrors; + return this.config.invalidateElementsAfterReadErrors; } @Override diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/AbstractOpenemsModbusComponent.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/AbstractOpenemsModbusComponent.java index 7bf6e9e249d..80c29c5f234 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/AbstractOpenemsModbusComponent.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/AbstractOpenemsModbusComponent.java @@ -110,17 +110,7 @@ protected void activate(String id) { protected boolean activate(ComponentContext context, String id, String alias, boolean enabled, int unitId, ConfigurationAdmin cm, String modbusReference, String modbusId) throws OpenemsException { super.activate(context, id, alias, enabled); - // update filter for 'Modbus' - if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), "Modbus", modbusId)) { - return true; - } - this.unitId = unitId; - var modbus = this.modbus.get(); - if (this.isEnabled() && modbus != null) { - modbus.addProtocol(this.id(), this.getModbusProtocol()); - modbus.retryModbusCommunication(this.id()); - } - return false; + return this.activateOrModified(unitId, cm, modbusReference, modbusId); } @Override @@ -147,19 +137,41 @@ protected void activate(ComponentContext context, String id, String alias, boole * @param modbusId The ID of the Modbus bridge. Typically * 'config.modbus_id()' * @return true if the target filter was updated. You may use it to abort the - * activate() method. + * modified() method. * @throws OpenemsException on error */ protected boolean modified(ComponentContext context, String id, String alias, boolean enabled, int unitId, ConfigurationAdmin cm, String modbusReference, String modbusId) throws OpenemsException { super.modified(context, id, alias, enabled); + return this.activateOrModified(unitId, cm, modbusReference, modbusId); + } + + @Override + protected void modified(ComponentContext context, String id, String alias, boolean enabled) { + throw new IllegalArgumentException("Use the other modified() for Modbus components!"); + } + + /** + * Common tasks for @Activate and @Modified. + * + * @param unitId Unit-ID of the Modbus target + * @param cm An instance of ConfigurationAdmin. Receive it + * using @Reference + * @param modbusReference The name of the @Reference setter method for the + * Modbus bridge - e.g. 'Modbus' if you have a + * setModbus()-method + * @param modbusId The ID of the Modbus bridge. Typically + * 'config.modbus_id()' + * @return true if the target filter was updated. You may use it to abort the + * activate() or modified() method. + */ + private boolean activateOrModified(int unitId, ConfigurationAdmin cm, String modbusReference, String modbusId) { // update filter for 'Modbus' - if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), "Modbus", modbusId)) { + if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), modbusReference, modbusId)) { return true; } this.unitId = unitId; var modbus = this.modbus.get(); - modbus.removeProtocol(this.id()); if (this.isEnabled() && modbus != null) { modbus.addProtocol(this.id(), this.getModbusProtocol()); modbus.retryModbusCommunication(this.id()); @@ -167,11 +179,6 @@ protected boolean modified(ComponentContext context, String id, String alias, bo return false; } - @Override - protected void modified(ComponentContext context, String id, String alias, boolean enabled) { - throw new IllegalArgumentException("Use the other activate() for Modbus components!"); - } - @Override protected void deactivate() { super.deactivate(); diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/Config.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/Config.java new file mode 100644 index 00000000000..9f6e98b6d45 --- /dev/null +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/Config.java @@ -0,0 +1,60 @@ +package io.openems.edge.bridge.modbus.api; + +import java.util.function.Supplier; + +import org.slf4j.Logger; + +public class Config { + + public final String id; + public final String alias; + public final boolean enabled; + public final int invalidateElementsAfterReadErrors; + public final LogHandler log; + + public Config(String id, String alias, boolean enabled, LogVerbosity logVerbosity, + int invalidateElementsAfterReadErrors) { + this.id = id; + this.alias = alias; + this.enabled = enabled; + this.invalidateElementsAfterReadErrors = invalidateElementsAfterReadErrors; + this.log = new LogHandler(this, logVerbosity); + } + + public static class LogHandler { + public final LogVerbosity verbosity; + + private final Config config; + + private LogHandler(Config config, LogVerbosity logVerbosity) { + this.config = config; + this.verbosity = logVerbosity; + } + + /** + * Logs messages for + * {@link LogVerbosity#READS_AND_WRITES_DURATION_TRACE_EVENTS}. + * + * @param logger the {@link Logger} + * @param message the String message + */ + public void trace(Logger logger, Supplier message) { + if (this.isTrace()) { + logger.info("[" + this.config.id + "] " + message.get()); + } + } + + /** + * Return true if {@link LogVerbosity#READS_AND_WRITES_DURATION_TRACE_EVENTS} is + * active. + * + * @return true for trace-log + */ + public boolean isTrace() { + return switch (this.verbosity) { + case NONE, DEBUG_LOG, READS_AND_WRITES, READS_AND_WRITES_DURATION, READS_AND_WRITES_VERBOSE -> false; + case READS_AND_WRITES_DURATION_TRACE_EVENTS -> true; + }; + } + } +} \ No newline at end of file diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/ModbusComponent.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/ModbusComponent.java index ace3a2c4da4..c2b284dd80f 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/ModbusComponent.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/ModbusComponent.java @@ -20,7 +20,13 @@ public interface ModbusComponent extends OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - MODBUS_COMMUNICATION_FAILED(Doc.of(Level.FAULT) // + + /* + * If ModbusCommunicationFault would be a FaultState, check it explicitly in + * Generic Ess ErrorHandler, as the battery could still have a communication + * fault while starting the battery + */ + MODBUS_COMMUNICATION_FAILED(Doc.of(Level.WARNING) // .debounce(10, Debounce.SAME_VALUES_IN_A_ROW_TO_CHANGE) // .text("Modbus Communication failed")) // ; diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/ModbusWorker.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/ModbusWorker.java index 68aed664b5d..2558852fc4d 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/ModbusWorker.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/ModbusWorker.java @@ -1,12 +1,12 @@ package io.openems.edge.bridge.modbus.api.worker; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import io.openems.common.worker.AbstractImmediateWorker; import io.openems.edge.bridge.modbus.api.BridgeModbus; -import io.openems.edge.bridge.modbus.api.LogVerbosity; +import io.openems.edge.bridge.modbus.api.Config.LogHandler; import io.openems.edge.bridge.modbus.api.ModbusComponent; import io.openems.edge.bridge.modbus.api.ModbusProtocol; import io.openems.edge.bridge.modbus.api.element.ModbusElement; @@ -52,18 +52,19 @@ public class ModbusWorker extends AbstractImmediateWorker { * @param cycleDelayChannel sets the * {@link BridgeModbus.ChannelId#CYCLE_DELAY} * channel - * @param logVerbosity the configured {@link LogVerbosity} + * @param logHandler a {@link Supplier} for the + * {@link LogHandler} */ public ModbusWorker(Function execute, Consumer invalidate, Consumer cycleTimeIsTooShortChannel, Consumer cycleDelayChannel, - AtomicReference logVerbosity) { + Supplier logHandler) { this.execute = execute; this.invalidate = invalidate; - this.defectiveComponents = new DefectiveComponents(logVerbosity); - this.tasksSupplier = new TasksSupplierImpl(); + this.defectiveComponents = new DefectiveComponents(logHandler); + this.tasksSupplier = new TasksSupplierImpl(logHandler); this.cycleTasksManager = new CycleTasksManager(this.tasksSupplier, this.defectiveComponents, - cycleTimeIsTooShortChannel, cycleDelayChannel, logVerbosity); + cycleTimeIsTooShortChannel, cycleDelayChannel, logHandler); } @Override @@ -123,7 +124,7 @@ private void markComponentAsDefective(ModbusComponent component, boolean isDefec * @param protocol the ModbusProtocol */ public void addProtocol(String sourceId, ModbusProtocol protocol) { - this.tasksSupplier.addProtocol(sourceId, protocol); + this.tasksSupplier.addProtocol(sourceId, protocol, this.invalidate); this.defectiveComponents.remove(sourceId); // Cleanup } @@ -133,7 +134,7 @@ public void addProtocol(String sourceId, ModbusProtocol protocol) { * @param sourceId Component-ID of the source */ public void removeProtocol(String sourceId) { - this.tasksSupplier.removeProtocol(sourceId); + this.tasksSupplier.removeProtocol(sourceId, this.invalidate); this.defectiveComponents.remove(sourceId); // Cleanup } diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/CycleTasksManager.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/CycleTasksManager.java index fbee14394ac..03222a82a04 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/CycleTasksManager.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/CycleTasksManager.java @@ -1,12 +1,12 @@ package io.openems.edge.bridge.modbus.api.worker.internal; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.openems.edge.bridge.modbus.api.LogVerbosity; +import io.openems.edge.bridge.modbus.api.Config.LogHandler; import io.openems.edge.bridge.modbus.api.task.Task; import io.openems.edge.bridge.modbus.api.task.WaitTask; import io.openems.edge.bridge.modbus.api.worker.ModbusWorker; @@ -26,7 +26,7 @@ public class CycleTasksManager { private final TasksSupplier tasksSupplier; private final DefectiveComponents defectiveComponents; private final Consumer cycleTimeIsTooShortChannel; - private final AtomicReference logVerbosity; + private final Supplier logHandler; private final WaitDelayHandler waitDelayHandler; private final WaitTask.Mutex waitMutexTask = new WaitTask.Mutex(); @@ -35,22 +35,15 @@ public class CycleTasksManager { public CycleTasksManager(TasksSupplier tasksSupplier, DefectiveComponents defectiveComponents, Consumer cycleTimeIsTooShortChannel, Consumer cycleDelayChannel, - AtomicReference logVerbosity) { + Supplier logHandler) { this.tasksSupplier = tasksSupplier; this.defectiveComponents = defectiveComponents; this.cycleTimeIsTooShortChannel = cycleTimeIsTooShortChannel; - this.logVerbosity = logVerbosity; - + this.logHandler = logHandler; this.waitDelayHandler = new WaitDelayHandler(() -> this.onWaitDelayTaskFinished(), cycleDelayChannel); } - protected CycleTasksManager(TasksSupplier tasksSupplier, DefectiveComponents defectiveComponents, - Consumer cycleTimeIsTooShortChannel, Consumer cycleDelayChannel) { - this(tasksSupplier, defectiveComponents, cycleTimeIsTooShortChannel, cycleDelayChannel, - new AtomicReference<>(LogVerbosity.NONE)); - } - - private static enum StateMachine { + protected static enum StateMachine { INITIAL_WAIT, // READ_BEFORE_WRITE, // WAIT_FOR_WRITE, // @@ -62,24 +55,31 @@ private static enum StateMachine { private StateMachine state = StateMachine.FINISHED; + /** + * Gets the current state. + * + * @return the {@link StateMachine} + */ + protected StateMachine getState() { + return this.state; + } + /** * Called on BEFORE_PROCESS_IMAGE event. */ public synchronized void onBeforeProcessImage() { // Calculate Delay - var waitDelayHandlerLog = this.waitDelayHandler.onBeforeProcessImage(this.isTraceLog()); + final var waitDelayHandlerLog = this.waitDelayHandler.onBeforeProcessImage(this.logHandler.get().isTrace()); // Evaluate Cycle-Time-Is-Too-Short, invalidate time measurement and stop early var cycleTimeIsTooShort = this.state != StateMachine.FINISHED; this.cycleTimeIsTooShortChannel.accept(cycleTimeIsTooShort); if (cycleTimeIsTooShort) { this.waitDelayHandler.timeIsInvalid(); - if (this.isTraceLog()) { - this.log.info("State: " + this.state + " unchanged" // - + " (in onBeforeProcessImage)" // - + " Delay [" + this.waitDelayHandler.getWaitDelayTask().initialDelay + "] " // - + waitDelayHandlerLog); - } + this.traceLog(() -> "State: " + this.state + " unchanged" // + + " (in onBeforeProcessImage)" // + + " Delay [" + this.waitDelayHandler.getWaitDelayTask().initialDelay + "] " // + + waitDelayHandlerLog); return; } @@ -90,18 +90,19 @@ public synchronized void onBeforeProcessImage() { this.cycleTasks = this.tasksSupplier.getCycleTasks(this.defectiveComponents); // On defectiveComponents invalidate time measurement - if (this.cycleTasks.containsDefectiveComponent(this.defectiveComponents)) { + final var containsDefectiveComponents = this.cycleTasks.containsDefectiveComponent(this.defectiveComponents); + if (containsDefectiveComponents) { this.waitDelayHandler.timeIsInvalid(); - waitDelayHandlerLog += " DEFECTIVE_COMPONENT"; } // Initialize next Cycle - if (this.isTraceLog()) { - this.log.info("State: " + this.state + " -> " + StateMachine.INITIAL_WAIT // - + " (in onBeforeProcessImage)" // - + " Delay [" + this.waitDelayHandler.getWaitDelayTask().initialDelay + "] " // - + waitDelayHandlerLog); - } + this.traceLog(() -> "State: " + this.state + " -> " + StateMachine.INITIAL_WAIT // + + " (in onBeforeProcessImage)" // + + " Delay [" + this.waitDelayHandler.getWaitDelayTask().initialDelay + "] " // + + waitDelayHandlerLog // + + (containsDefectiveComponents // + ? " DEFECTIVE_COMPONENT" + : "")); this.state = StateMachine.INITIAL_WAIT; // Interrupt wait @@ -112,9 +113,7 @@ public synchronized void onBeforeProcessImage() { * Called on EXECUTE_WRITE event. */ public synchronized void onExecuteWrite() { - if (this.isTraceLog()) { - this.log.info("State: " + this.state + " -> " + StateMachine.WRITE + " (onExecuteWrite)"); - } + this.traceLog(() -> "State: " + this.state + " -> " + StateMachine.WRITE + " (onExecuteWrite)"); this.state = StateMachine.WRITE; this.waitMutexTask.release(); @@ -128,7 +127,8 @@ public synchronized void onExecuteWrite() { */ public Task getNextTask() { if (this.cycleTasks == null) { - return this.waitMutexTask; + // Fallback to avoid NPE on race condition + this.cycleTasks = this.tasksSupplier.getCycleTasks(this.defectiveComponents); } var previousState = this.state; // drop before release @@ -187,8 +187,8 @@ public Task getNextTask() { } }; - if (this.state != previousState && this.isTraceLog()) { - this.log.info("State: " + previousState + " -> " + this.state + " (getNextTask)"); + if (this.state != previousState) { + this.traceLog(() -> "State: " + previousState + " -> " + this.state + " (getNextTask)"); } return nextTask; } @@ -207,16 +207,11 @@ private synchronized void onWaitDelayTaskFinished() { }; if (this.state != previousState) { - if (this.isTraceLog()) { - this.log.info("State: " + previousState + " -> " + this.state + " (onWaitDelayTaskFinished)"); - } + this.traceLog(() -> "State: " + previousState + " -> " + this.state + " (onWaitDelayTaskFinished)"); } } - private boolean isTraceLog() { - return switch (this.logVerbosity.get()) { - case READS_AND_WRITES_DURATION_TRACE_EVENTS -> true; - case NONE, DEBUG_LOG, READS_AND_WRITES, READS_AND_WRITES_DURATION, READS_AND_WRITES_VERBOSE -> false; - }; + private void traceLog(Supplier message) { + this.logHandler.get().trace(this.log, message); } } \ No newline at end of file diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponents.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponents.java index 6fa218f805f..80cb95937df 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponents.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponents.java @@ -4,12 +4,12 @@ import java.time.Instant; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.openems.edge.bridge.modbus.api.LogVerbosity; +import io.openems.edge.bridge.modbus.api.Config.LogHandler; import io.openems.edge.common.type.TypeUtils; public class DefectiveComponents { @@ -23,24 +23,16 @@ private static record NextTry(Instant timestamp, int count) { } private final Clock clock; - private final AtomicReference logVerbosity; + private final Supplier logHandler; private final Map nextTries = new HashMap<>(); - public DefectiveComponents(AtomicReference logVerbosity) { - this(Clock.systemDefaultZone(), logVerbosity); + public DefectiveComponents(Supplier logHandler) { + this(Clock.systemDefaultZone(), logHandler); } - protected DefectiveComponents() { - this(Clock.systemDefaultZone(), new AtomicReference<>(LogVerbosity.READS_AND_WRITES_DURATION_TRACE_EVENTS)); - } - - protected DefectiveComponents(Clock clock) { - this(clock, new AtomicReference<>(LogVerbosity.READS_AND_WRITES_DURATION_TRACE_EVENTS)); - } - - protected DefectiveComponents(Clock clock, AtomicReference logVerbosity) { + protected DefectiveComponents(Clock clock, Supplier logHandler) { this.clock = clock; - this.logVerbosity = logVerbosity; + this.logHandler = logHandler; } /** @@ -54,15 +46,11 @@ public synchronized void add(String componentId) { this.nextTries.compute(componentId, (k, v) -> { var count = (v == null) ? 1 : v.count + 1; var wait = Math.min(INCREASE_WAIT_SECONDS * count, MAX_WAIT_SECONDS); - if (this.isTraceLog()) { - final String log; - if (count == 1) { - log = "Add [" + componentId + "] to defective Components."; - } else { - log = "Increase wait for defective Component [" + componentId + "]."; - } - this.log.info(log + " Wait [" + wait + "s]" + " Count [" + count + "]"); - } + this.traceLog(() -> // + (count == 1 // + ? "Add [" + componentId + "] to defective Components." // + : "Increase wait for defective Component [" + componentId + "].") + " Wait [" + wait + "s]" + + " Count [" + count + "]"); return new NextTry(Instant.now(this.clock).plusSeconds(wait), count); }); } @@ -74,8 +62,8 @@ public synchronized void add(String componentId) { */ public synchronized void remove(String componentId) { TypeUtils.assertNull("DefectiveComponents remove() takes no null values", componentId); - if (this.nextTries.remove(componentId) != null && this.isTraceLog()) { - this.log.info("Remove [" + componentId + "] from defective Components."); + if (this.nextTries.remove(componentId) != null) { + this.traceLog(() -> "Remove [" + componentId + "] from defective Components."); } } @@ -105,12 +93,7 @@ public synchronized Boolean isDueForNextTry(String componentId) { return now.isAfter(nextTry.timestamp); } - private boolean isTraceLog() { - return switch (this.logVerbosity.get()) { - case READS_AND_WRITES, READS_AND_WRITES_DURATION, READS_AND_WRITES_VERBOSE, - READS_AND_WRITES_DURATION_TRACE_EVENTS -> - true; - case NONE, DEBUG_LOG -> false; - }; + private void traceLog(Supplier message) { + this.logHandler.get().trace(this.log, message); } } \ No newline at end of file diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImpl.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImpl.java index e7c6a4bcb02..2f836ee0c14 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImpl.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImpl.java @@ -5,9 +5,16 @@ import java.util.LinkedList; import java.util.Map; import java.util.Queue; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.edge.bridge.modbus.api.Config.LogHandler; import io.openems.edge.bridge.modbus.api.ModbusProtocol; +import io.openems.edge.bridge.modbus.api.element.ModbusElement; import io.openems.edge.bridge.modbus.api.task.ReadTask; import io.openems.edge.bridge.modbus.api.task.Task; import io.openems.edge.bridge.modbus.api.task.WriteTask; @@ -20,6 +27,13 @@ */ public class TasksSupplierImpl implements TasksSupplier { + private final Logger log = LoggerFactory.getLogger(TasksSupplierImpl.class); + private final Supplier logHandler; + + public TasksSupplierImpl(Supplier logHandler) { + this.logHandler = logHandler; + } + /** * Source-ID -> TasksManager for {@link Task}s. */ @@ -31,22 +45,45 @@ public class TasksSupplierImpl implements TasksSupplier { private final Queue> nextLowPriorityTasks = new LinkedList<>(); /** - * Adds the protocol. - * - * @param sourceId Component-ID of the source - * @param protocol the ModbusProtocol + * Adds (or replaces) the protocol identified by its sourceId. + * + *

+ * If a protocol with the same sourceId existed before, + * {@link #removeProtocol(String, Consumer)} is called internally first. + * + * @param sourceId Component-ID of the source + * @param protocol the ModbusProtocol + * @param invalidate invalidates the given {@link ModbusElement}s after read + * errors */ - public synchronized void addProtocol(String sourceId, ModbusProtocol protocol) { + public synchronized void addProtocol(String sourceId, ModbusProtocol protocol, + Consumer invalidate) { + this.removeProtocol(sourceId, invalidate); // remove if sourceId exists + + this.traceLog(() -> "Add Protocol for " // + + "[" + sourceId + "] with " // + + "[" + protocol.getTaskManager().countTasks() + "] tasks"); this.taskManagers.put(sourceId, protocol.getTaskManager()); } /** - * Removes the protocol. + * Removes the protocol and invalidates all {@link ModbusElement}s. * - * @param sourceId Component-ID of the source + * @param sourceId Component-ID of the source + * @param invalidate invalidates the given {@link ModbusElement}s after read + * errors */ - public synchronized void removeProtocol(String sourceId) { - this.taskManagers.remove(sourceId); + public synchronized void removeProtocol(String sourceId, Consumer invalidate) { + var taskManager = this.taskManagers.remove(sourceId); + if (taskManager == null) { + return; + } + + this.traceLog(() -> "Remove Protocol for " // + + "[" + sourceId + "] with " // + + "[" + taskManager.countTasks() + "] tasks"); + taskManager.getTasks() // + .forEach(t -> invalidate.accept(t.getElements())); this.nextLowPriorityTasks.removeIf(t -> t.a() == sourceId); } @@ -84,7 +121,7 @@ public synchronized CycleTasks getCycleTasks(DefectiveComponents defectiveCompon componentTasks.clear(); } }); - return new CycleTasks(// + var result = new CycleTasks(// tasks.values().stream().flatMap(LinkedList::stream) // .filter(ReadTask.class::isInstance).map(ReadTask.class::cast) // // Sort HIGH priority to the end @@ -93,6 +130,11 @@ public synchronized CycleTasks getCycleTasks(DefectiveComponents defectiveCompon tasks.values().stream().flatMap(LinkedList::stream) // .filter(WriteTask.class::isInstance).map(WriteTask.class::cast) // .collect(Collectors.toCollection(LinkedList::new))); + + this.traceLog(() -> "Getting " // + + "[" + result.reads().size() + "] read and " // + + "[" + result.writes().size() + "] write tasks for this Cycle"); + return result; } /** @@ -128,4 +170,8 @@ public synchronized int getTotalNumberOfTasks() { .mapToInt(m -> m.countTasks()) // .sum(); } + + private void traceLog(Supplier message) { + this.logHandler.get().trace(this.log, message); + } } diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/test/DummyModbusBridge.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/test/DummyModbusBridge.java index 1d508bb9363..e41bae3848c 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/test/DummyModbusBridge.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/test/DummyModbusBridge.java @@ -1,25 +1,60 @@ package io.openems.edge.bridge.modbus.test; +import static io.openems.common.utils.ReflectionUtils.getValueViaReflection; +import static io.openems.edge.common.event.EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE; + import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.HashMap; -import java.util.Map; +import java.util.function.Consumer; + +import org.osgi.service.event.Event; +import com.ghgande.j2mod.modbus.ModbusException; import com.ghgande.j2mod.modbus.io.ModbusTransaction; +import com.ghgande.j2mod.modbus.msg.ModbusRequest; +import com.ghgande.j2mod.modbus.msg.ModbusResponse; +import com.ghgande.j2mod.modbus.net.AbstractModbusListener; +import com.ghgande.j2mod.modbus.procimg.ProcessImage; +import com.ghgande.j2mod.modbus.procimg.SimpleProcessImage; +import com.ghgande.j2mod.modbus.procimg.SimpleRegister; import io.openems.common.exceptions.OpenemsException; import io.openems.edge.bridge.modbus.api.AbstractModbusBridge; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.BridgeModbusTcp; +import io.openems.edge.bridge.modbus.api.Config; import io.openems.edge.bridge.modbus.api.LogVerbosity; -import io.openems.edge.bridge.modbus.api.ModbusProtocol; -import io.openems.edge.common.channel.Channel; +import io.openems.edge.bridge.modbus.api.worker.internal.DefectiveComponents; +import io.openems.edge.bridge.modbus.api.worker.internal.TasksSupplier; import io.openems.edge.common.component.OpenemsComponent; public class DummyModbusBridge extends AbstractModbusBridge implements BridgeModbusTcp, BridgeModbus, OpenemsComponent { - private final Map protocols = new HashMap<>(); + private final ModbusTransaction modbusTransaction = new ModbusTransaction() { + @Override + public void execute() throws ModbusException { + this.response = DummyModbusBridge.this.executeModbusRequest(this.request); + } + }; + private final AbstractModbusListener modbusListener = new AbstractModbusListener() { + @Override + public ProcessImage getProcessImage(int unitId) { + return DummyModbusBridge.this.processImage; + } + @Override + public void run() { + } + + @Override + public void stop() { + } + + }; + private final TasksSupplier tasksSupplier; + private final DefectiveComponents defectiveComponents; + + private SimpleProcessImage processImage = null; private InetAddress ipAddress = null; public DummyModbusBridge(String id) { @@ -32,10 +67,20 @@ public DummyModbusBridge(String id, LogVerbosity logVerbosity) { BridgeModbus.ChannelId.values(), // BridgeModbusTcp.ChannelId.values() // ); - for (Channel channel : this.channels()) { + for (var channel : this.channels()) { channel.nextProcessImage(); } - super.activate(null, id, "", true, logVerbosity, 2); + super.activate(null, new Config(id, "", false, logVerbosity, 2)); + this.tasksSupplier = getValueViaReflection(this.worker, "tasksSupplier"); + this.defectiveComponents = getValueViaReflection(this.worker, "defectiveComponents"); + } + + private synchronized DummyModbusBridge withProcessImage(Consumer callback) { + if (this.processImage == null) { + this.processImage = new SimpleProcessImage(); + } + callback.accept(this.processImage); + return this; } /** @@ -50,14 +95,82 @@ public DummyModbusBridge withIpAddress(String ipAddress) throws UnknownHostExcep return this; } - @Override - public void addProtocol(String sourceId, ModbusProtocol protocol) { - this.protocols.put(sourceId, protocol); + /** + * Sets the value of a Register. + * + * @param address the Register address + * @param b1 first byte + * @param b2 second byte + * @return myself + */ + public DummyModbusBridge withRegister(int address, byte b1, byte b2) { + return this.withProcessImage(pi -> pi.addRegister(address, new SimpleRegister(b1, b2))); + } + + /** + * Sets the value of a Register. + * + * @param address the Register address + * @param value the value + * @return myself + */ + public DummyModbusBridge withRegister(int address, int value) { + return this.withProcessImage(pi -> pi.addRegister(address, new SimpleRegister(value))); } + /** + * Sets the values of Registers. + * + * @param startAddress the start Register address + * @param values the values + * @return myself + */ + public DummyModbusBridge withRegisters(int startAddress, int... values) { + for (var value : values) { + this.withRegister(startAddress++, value); + } + return this; + } + + /** + * Sets the values of Registers. + * + * @param startAddress the start Register address + * @param values the values + * @return myself + */ + public DummyModbusBridge withRegisters(int startAddress, int[]... values) { + for (var a : values) { + for (var b : a) { + this.withRegister(startAddress++, b); + } + } + return this; + } + + /** + * NOTE: {@link DummyModbusBridge} does not call parent handleEvent(). + */ @Override - public void removeProtocol(String sourceId) { - this.protocols.remove(sourceId); + public synchronized void handleEvent(Event event) { + // NOTE: TOPIC_CYCLE_EXECUTE_WRITE is not implemented (yet) + if (this.processImage == null) { + return; + } + switch (event.getTopic()) { + case TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> this.onBeforeProcessImage(); + } + } + + private ModbusResponse executeModbusRequest(ModbusRequest request) { + return request.createResponse(this.modbusListener); + } + + private void onBeforeProcessImage() { + var cycleTasks = this.tasksSupplier.getCycleTasks(this.defectiveComponents); + for (var readTask : cycleTasks.reads()) { + readTask.execute(this); + } } @Override @@ -70,12 +183,11 @@ public InetAddress getIpAddress() { @Override public ModbusTransaction getNewModbusTransaction() throws OpenemsException { - throw new UnsupportedOperationException("getNewModbusTransaction() Unsupported by Dummy Class"); + return this.modbusTransaction; } @Override public void closeModbusConnection() { - throw new UnsupportedOperationException("closeModbusConnection() Unsupported by Dummy Class"); } } diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/BridgeModbusSerialImplTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/BridgeModbusSerialImplTest.java index 3645d2d6fdf..1d6d1e780ba 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/BridgeModbusSerialImplTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/BridgeModbusSerialImplTest.java @@ -9,13 +9,11 @@ public class BridgeModbusSerialImplTest { - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new BridgeModbusSerialImpl()) // .activate(MyConfigSerial.create() // - .setId(MODBUS_ID) // + .setId("modbus0") // .setPortName("/etc/ttyUSB0") // .setBaudRate(9600) // .setDatabits(8) // diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/BridgeModbusTcpImplTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/BridgeModbusTcpImplTest.java index 276dc8dcce6..33c3496c644 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/BridgeModbusTcpImplTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/BridgeModbusTcpImplTest.java @@ -1,6 +1,7 @@ package io.openems.edge.bridge.modbus; -import org.junit.Ignore; +import static io.openems.edge.bridge.modbus.api.ModbusComponent.ChannelId.MODBUS_COMMUNICATION_FAILED; + import org.junit.Test; import com.ghgande.j2mod.modbus.procimg.Register; @@ -11,7 +12,6 @@ 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.bridge.modbus.api.AbstractModbusBridge; import io.openems.edge.bridge.modbus.api.LogVerbosity; @@ -26,16 +26,9 @@ public class BridgeModbusTcpImplTest { - private static final String MODBUS_ID = "modbus0"; - private static final String DEVICE_ID = "device0"; private static final int UNIT_ID = 1; private static final int CYCLE_TIME = 100; - private static final ChannelAddress REGISTER_100 = new ChannelAddress(DEVICE_ID, "Register100"); - private static final ChannelAddress MODBUS_COMMUNICATION_FAILED = new ChannelAddress(DEVICE_ID, - "ModbusCommunicationFailed"); - - @Ignore @Test public void test() throws Exception { final ThrowingRunnable sleep = () -> Thread.sleep(CYCLE_TIME); @@ -57,16 +50,15 @@ public void test() throws Exception { * Instantiate Modbus-Bridge */ var sut = new BridgeModbusTcpImpl(); - var device = new MyModbusComponent(DEVICE_ID, sut, UNIT_ID); var test = new ComponentTest(sut) // - .addComponent(device) // .activate(MyConfigTcp.create() // - .setId(MODBUS_ID) // + .setId("modbus0") // .setIp("127.0.0.1") // .setPort(port) // .setInvalidateElementsAfterReadErrors(1) // .setLogVerbosity(LogVerbosity.NONE) // .build()); + test.addComponent(new MyModbusComponent("device0", sut, UNIT_ID)); /* * Successfully read Register @@ -76,34 +68,22 @@ public void test() throws Exception { .onAfterProcessImage(sleep)) // .next(new TestCase() // .onAfterProcessImage(sleep) // - .output(REGISTER_100, 123) // - .output(MODBUS_COMMUNICATION_FAILED, false)); // + .output("device0", MyModbusComponent.ChannelId.REGISTER_100, 123) // + .output("device0", MODBUS_COMMUNICATION_FAILED, false)); // /* - * Reading Register fails after debounce of 10 + * Remove Protocol and unset channel values */ - processImage.removeRegister(register100); - for (var i = 0; i < 9; i++) { - test.next(new TestCase() // - .onAfterProcessImage(sleep)); - } + sut.removeProtocol("device0"); + test // .next(new TestCase() // - .onAfterProcessImage(sleep) // - .output(MODBUS_COMMUNICATION_FAILED, false)) // + .onAfterProcessImage(sleep)) // .next(new TestCase() // .onAfterProcessImage(sleep) // - .output(MODBUS_COMMUNICATION_FAILED, true)); + .output("device0", MyModbusComponent.ChannelId.REGISTER_100, null) // + .output("device0", MODBUS_COMMUNICATION_FAILED, false)); // - /* - * Successfully read Register - */ - processImage.addRegister(100, register100); - test // - .next(new TestCase() // - .onAfterProcessImage(sleep) // - .output(REGISTER_100, 123) // - .output(MODBUS_COMMUNICATION_FAILED, false)); // } finally { if (slave != null) { slave.close(); diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/element/BitsWordElementTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/element/BitsWordElementTest.java index 65b0e885a56..d3f068826da 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/element/BitsWordElementTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/element/BitsWordElementTest.java @@ -3,6 +3,7 @@ import static io.openems.common.channel.AccessMode.READ_WRITE; import static io.openems.common.types.OpenemsType.BOOLEAN; import static io.openems.common.types.OpenemsType.INTEGER; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -129,12 +130,7 @@ public void testNotBoolean() throws Exception { private static ModbusTest.FC3ReadRegisters generateSut() throws IllegalArgumentException, IllegalAccessException, OpenemsException, NoSuchFieldException, SecurityException { var sut = new ModbusTest.FC3ReadRegisters<>(new BitsWordElement(0, null), INTEGER); - - // Some Reflection to properly initialize the BitsWordElement - var field = BitsWordElement.class.getDeclaredField("component"); - field.setAccessible(true); - field.set(sut.element, sut); - + setAttributeViaReflection(sut.element, "component", sut); return sut; } diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/CycleTasksManagerTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/CycleTasksManagerTest.java index 0d764fdecef..e57a4b32917 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/CycleTasksManagerTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/CycleTasksManagerTest.java @@ -1,15 +1,21 @@ package io.openems.edge.bridge.modbus.api.worker.internal; +import static io.openems.edge.bridge.modbus.api.worker.internal.CycleTasksManager.StateMachine.FINISHED; +import static io.openems.edge.bridge.modbus.api.worker.internal.CycleTasksManager.StateMachine.WAIT_BEFORE_READ; +import static io.openems.edge.bridge.modbus.api.worker.internal.CycleTasksManager.StateMachine.WRITE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.function.Consumer; +import java.util.function.Supplier; import org.junit.Before; import org.junit.Test; import io.openems.common.exceptions.OpenemsException; import io.openems.edge.bridge.modbus.DummyModbusComponent; +import io.openems.edge.bridge.modbus.api.Config; +import io.openems.edge.bridge.modbus.api.LogVerbosity; import io.openems.edge.bridge.modbus.api.task.WaitTask; import io.openems.edge.bridge.modbus.api.worker.DummyReadTask; import io.openems.edge.bridge.modbus.api.worker.DummyWriteTask; @@ -21,6 +27,8 @@ public class CycleTasksManagerTest { }; public static final Consumer CYCLE_DELAY = (cycleDelay) -> { }; + private static final Config CONFIG = new Config("foo", "bar", true, LogVerbosity.NONE, 1); + public static final Supplier LOG_HANDLER = () -> CONFIG.log; private static DummyReadTask RT_H_1; private static DummyReadTask RT_H_2; @@ -48,9 +56,10 @@ public void testIdealConditions() throws OpenemsException, InterruptedException .writes(WT_1) // .build(); var tasksSupplier = new DummyTasksSupplier(cycle1, cycle2); - var defectiveComponents = new DefectiveComponents(); + var defectiveComponents = new DefectiveComponents(LOG_HANDLER); - var sut = new CycleTasksManager(tasksSupplier, defectiveComponents, CYCLE_TIME_IS_TOO_SHORT, CYCLE_DELAY); + var sut = new CycleTasksManager(tasksSupplier, defectiveComponents, // + CYCLE_TIME_IS_TOO_SHORT, CYCLE_DELAY, LOG_HANDLER); // Cycle 1 sut.onBeforeProcessImage(); @@ -117,6 +126,32 @@ public void testIdealConditions() throws OpenemsException, InterruptedException // task.execute(null); -> this would block in single-threaded JUnit test } + @Test + public void testExecuteWriteBeforeNextProcessImage() throws OpenemsException, InterruptedException { + var cycle1 = CycleTasks.create() // + .reads(RT_L_1, RT_H_1, RT_H_2) // + .writes(WT_1) // + .build(); + var cycle2 = CycleTasks.create() // + .reads(RT_L_2, RT_H_1, RT_H_2) // + .writes(WT_1) // + .build(); + var tasksSupplier = new DummyTasksSupplier(cycle1, cycle2); + var defectiveComponents = new DefectiveComponents(LOG_HANDLER); + + var sut = new CycleTasksManager(tasksSupplier, defectiveComponents, // + CYCLE_TIME_IS_TOO_SHORT, CYCLE_DELAY, LOG_HANDLER); + + sut.getNextTask(); + assertEquals(FINISHED, sut.getState()); + sut.onExecuteWrite(); + sut.getNextTask(); + assertEquals(WRITE, sut.getState()); + sut.onBeforeProcessImage(); + sut.getNextTask(); + assertEquals(WAIT_BEFORE_READ, sut.getState()); + } + @Test public void testDefective() throws OpenemsException, InterruptedException { var component = new DummyModbusComponent(); @@ -131,10 +166,11 @@ public void testDefective() throws OpenemsException, InterruptedException { .writes(WT_1) // .build(); var tasksSupplier = new DummyTasksSupplier(cycle1, cycle2); - var defectiveComponents = new DefectiveComponents(); + var defectiveComponents = new DefectiveComponents(LOG_HANDLER); defectiveComponents.add(component.id()); - var sut = new CycleTasksManager(tasksSupplier, defectiveComponents, CYCLE_TIME_IS_TOO_SHORT, CYCLE_DELAY); + var sut = new CycleTasksManager(tasksSupplier, defectiveComponents, // + CYCLE_TIME_IS_TOO_SHORT, CYCLE_DELAY, LOG_HANDLER); // Cycle 1 sut.onBeforeProcessImage(); @@ -162,9 +198,10 @@ public void testNoTasks() throws OpenemsException, InterruptedException { var cycle1 = CycleTasks.create() // .build(); var tasksSupplier = new DummyTasksSupplier(cycle1); - var defectiveComponents = new DefectiveComponents(); + var defectiveComponents = new DefectiveComponents(LOG_HANDLER); - var sut = new CycleTasksManager(tasksSupplier, defectiveComponents, CYCLE_TIME_IS_TOO_SHORT, CYCLE_DELAY); + var sut = new CycleTasksManager(tasksSupplier, defectiveComponents, // + CYCLE_TIME_IS_TOO_SHORT, CYCLE_DELAY, LOG_HANDLER); // Cycle 1 sut.onBeforeProcessImage(); diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponentsTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponentsTest.java index 3f8d57ea014..ce0b9f97068 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponentsTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponentsTest.java @@ -1,5 +1,7 @@ package io.openems.edge.bridge.modbus.api.worker.internal; +import static io.openems.edge.bridge.modbus.api.worker.internal.CycleTasksManagerTest.LOG_HANDLER; +import static io.openems.edge.common.test.TestUtils.createDummyClock; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -8,16 +10,14 @@ import org.junit.Test; -import io.openems.common.test.TimeLeapClock; - public class DefectiveComponentsTest { private static final String CMP = "foo"; @Test public void testIsDueForNextTry() { - var clock = new TimeLeapClock(); - var sut = new DefectiveComponents(clock); + final var clock = createDummyClock(); + var sut = new DefectiveComponents(clock, LOG_HANDLER); assertNull(sut.isDueForNextTry(CMP)); sut.add(CMP); @@ -28,8 +28,8 @@ public void testIsDueForNextTry() { @Test public void testAddRemove() { - var clock = new TimeLeapClock(); - var sut = new DefectiveComponents(clock); + final var clock = createDummyClock(); + var sut = new DefectiveComponents(clock, LOG_HANDLER); sut.add(CMP); clock.leap(30_001, ChronoUnit.MILLIS); @@ -40,7 +40,7 @@ public void testAddRemove() { @Test public void testIsKnownw() { - var sut = new DefectiveComponents(); + var sut = new DefectiveComponents(LOG_HANDLER); sut.add(CMP); assertTrue(sut.isKnown(CMP)); diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImplTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImplTest.java index 0af815bda47..77e3d8b8f3f 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImplTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImplTest.java @@ -1,5 +1,7 @@ package io.openems.edge.bridge.modbus.api.worker.internal; +import static io.openems.edge.bridge.modbus.api.worker.internal.CycleTasksManagerTest.LOG_HANDLER; +import static io.openems.edge.common.test.TestUtils.createDummyClock; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -11,6 +13,7 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.test.TimeLeapClock; +import io.openems.common.utils.FunctionUtils; import io.openems.edge.bridge.modbus.DummyModbusComponent; import io.openems.edge.bridge.modbus.api.worker.DummyReadTask; import io.openems.edge.bridge.modbus.api.worker.DummyWriteTask; @@ -35,14 +38,14 @@ public void before() { @Test public void testFull() throws OpenemsException { - var clock = new TimeLeapClock(); - var defectiveComponents = new DefectiveComponents(clock); - var sut = new TasksSupplierImpl(); + final var clock = createDummyClock(); + var defectiveComponents = new DefectiveComponents(clock, LOG_HANDLER); + var sut = new TasksSupplierImpl(LOG_HANDLER); var component = new DummyModbusComponent(); var protocol = component.getModbusProtocol(); protocol.addTasks(RT_H_1, RT_H_2, RT_L_1, RT_L_2, WT_1); - sut.addProtocol(component.id(), protocol); + sut.addProtocol(component.id(), protocol, FunctionUtils::doNothing); // 1st Cycle var tasks = sut.getCycleTasks(defectiveComponents); @@ -82,19 +85,19 @@ public void testFull() throws OpenemsException { assertEquals(4, tasks.reads().size() + tasks.writes().size()); // Finish - sut.removeProtocol(component.id()); + sut.removeProtocol(component.id(), FunctionUtils::doNothing); } @Test public void testHighOnly() throws OpenemsException { var clock = new TimeLeapClock(); - var defectiveComponents = new DefectiveComponents(clock); - var sut = new TasksSupplierImpl(); + var defectiveComponents = new DefectiveComponents(clock, LOG_HANDLER); + var sut = new TasksSupplierImpl(LOG_HANDLER); var component = new DummyModbusComponent(); var protocol = component.getModbusProtocol(); protocol.addTasks(RT_H_1, RT_H_2, WT_1); - sut.addProtocol(component.id(), protocol); + sut.addProtocol(component.id(), protocol, FunctionUtils::doNothing); var tasks = sut.getCycleTasks(defectiveComponents); assertEquals(3, tasks.reads().size() + tasks.writes().size()); diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java index 2ce0c196ec0..2e877e2cb42 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java @@ -1,12 +1,12 @@ package io.openems.edge.bridge.modbus.sunspec; import static io.openems.edge.bridge.modbus.sunspec.AbstractOpenemsSunSpecComponent.preprocessModbusElements; +import static java.util.stream.IntStream.range; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import java.util.ArrayList; -import java.util.stream.IntStream; import org.junit.Before; import org.junit.Ignore; @@ -24,9 +24,16 @@ import io.openems.edge.bridge.modbus.api.LogVerbosity; import io.openems.edge.bridge.modbus.api.element.ModbusElement; import io.openems.edge.bridge.modbus.api.element.StringWordElement; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S1; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S101; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S103; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S701; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S701_ACType; import io.openems.edge.bridge.modbus.sunspec.Point.ModbusElementPoint; import io.openems.edge.bridge.modbus.sunspec.dummy.MyConfig; import io.openems.edge.bridge.modbus.sunspec.dummy.MySunSpecComponentImpl; +import io.openems.edge.bridge.modbus.test.DummyModbusBridge; +import io.openems.edge.common.channel.ChannelId; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -60,6 +67,76 @@ public void changeLogLevel() { java.lang.System.setProperty("org.ops4j.pax.logging.DefaultServiceLog.level", "INFO"); } + @Test + public void testReadFromModbus() throws Exception { + var sut = new MySunSpecComponentImpl(); + new ComponentTest(sut) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("setModbus", new DummyModbusBridge("modbus0") // + .withRegisters(40000, 0x5375, 0x6e53) // isSunSpec + .withRegisters(40002, 1, 66) // Block 1 + .withRegisters(40004, // + new int[] { 0x4D79, 0x204D, 0x616E, 0x7566, 0x6163, 0x7475, 0x7265, 0x7200, 0, 0, 0, 0, + 0, 0, 0, 0 }, // S1_MN + new int[] { 0x4D79, 0x204D, 0x6F64, 0x656C, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // S1_MD + range(0, 43).map(i -> 0).toArray()) // + .withRegisters(40070, 101, 50) // Block 101 + .withRegisters(40072, // + new int[] { 123, 234, 345, 456, 1 }, // + range(0, 45).map(i -> 0).toArray()) // + .withRegisters(40122, 103, 50) // Block 103 + .withRegisters(40124, // + new int[] { 124, 235, 346, 457, 1 }, // + range(0, 45).map(i -> 0).toArray()) // + .withRegisters(40174, 701, 121) // Block 701 + .withRegisters(40176, // + new int[] { 1, }, // + range(0, 120).map(i -> 0).toArray()) // + .withRegisters(40297, 702, 50) // Block 702 + .withRegisters(40299, // + new int[] { 1, }, // + range(0, 49).map(i -> 0).toArray()) // + .withRegisters(40375, 0xFFFF) // END_OF_MAP + ) // + .activate(MyConfig.create() // + .setId("cmp0") // + .setModbusId("modbus0") // + .setModbusUnitId(UNIT_ID) // + .setReadFromModbusBlock(1) // + .build()) + + .next(new TestCase()) // + .next(new TestCase() // + .output(c(S1.MN), null) // + .output(c(S1.MD), null)) + + .next(new TestCase() // + .output(c(S1.MN), "My Manufacturer") // + .output(c(S1.MD), "My Model")) + + .next(new TestCase() // + .output(c(S101.A), null) // + .output(c(S101.APH_A), null) // + .output(c(S103.A), null) // + .output(c(S103.APH_A), null)) // + + .next(new TestCase() // + .output(c(S101.A), 1230F) // + .output(c(S101.APH_A), 2340F) // + .output(c(S701.A_C_TYPE), S701_ACType.UNDEFINED)) // + + .next(new TestCase() // + .output(c(S103.A), 1240F) // + .output(c(S103.APH_A), 2350F) // + .output(c(S701.A_C_TYPE), S701_ACType.SPLIT_PHASE)) // + + .deactivate(); + } + + private static ChannelId c(SunSpecPoint point) { + return point.getChannelId(); + } + private static ImmutableSortedMap.Builder generateSunSpec() { var b = ImmutableSortedMap.naturalOrder() // .put(40000, 0x5375) // SunSpec identifier @@ -67,18 +144,18 @@ private static ImmutableSortedMap.Builder generateSunSpec() { .put(40002, 1) // SunSpec Block-ID .put(40003, 66); // Length of the SunSpec Block - IntStream.range(40004, 40070).forEach(i -> b.put(i, 0)); + range(40004, 40070).forEach(i -> b.put(i, 0)); b // .put(40070, 103) // SunSpec Block-ID .put(40071, 24); // Length of the SunSpec Block - IntStream.range(40072, 40096).forEach(i -> b.put(i, 0)); + range(40072, 40096).forEach(i -> b.put(i, 0)); b // .put(40096, 999) // SunSpec Block-ID .put(40097, 10) // Length of the SunSpec Block .put(40108, 702) // SunSpec Block-ID .put(40109, 50); // Length of the SunSpec Block - IntStream.range(40110, 40160).forEach(i -> b.put(i, 0)); + range(40110, 40160).forEach(i -> b.put(i, 0)); return b; } diff --git a/io.openems.edge.bridge.onewire/test/io/openems/edge/bridge/onewire/impl/BridgeOnewireImplTest.java b/io.openems.edge.bridge.onewire/test/io/openems/edge/bridge/onewire/impl/BridgeOnewireImplTest.java index 744192de7c8..c81a5ca33c2 100644 --- a/io.openems.edge.bridge.onewire/test/io/openems/edge/bridge/onewire/impl/BridgeOnewireImplTest.java +++ b/io.openems.edge.bridge.onewire/test/io/openems/edge/bridge/onewire/impl/BridgeOnewireImplTest.java @@ -6,13 +6,11 @@ public class BridgeOnewireImplTest { - private static final String BRIDGE_ID = "onewire0"; - @Test public void test() throws Exception { new ComponentTest(new BridgeOnewireImpl()) // .activate(MyConfig.create() // - .setId(BRIDGE_ID) // + .setId("onewire0") // .setPort("USB1") // .build()) // ; diff --git a/io.openems.edge.common/src/io/openems/edge/common/currency/Currency.java b/io.openems.edge.common/src/io/openems/edge/common/currency/Currency.java index 1aaa95601f7..52337d0c78f 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/currency/Currency.java +++ b/io.openems.edge.common/src/io/openems/edge/common/currency/Currency.java @@ -1,5 +1,6 @@ package io.openems.edge.common.currency; +import io.openems.common.types.CurrencyConfig; import io.openems.common.types.OptionsEnum; public enum Currency implements OptionsEnum { @@ -30,4 +31,18 @@ public OptionsEnum getUndefined() { return Currency.UNDEFINED; } + /** + * Converts the {@link CurrencyConfig} to the {@link Currency}. + * + * @param config currencyConfig to be transformed + * @return The {@link Currency}. + */ + public static Currency fromCurrencyConfig(CurrencyConfig config) { + return switch (config) { + case EUR -> Currency.EUR; + case SEK -> Currency.SEK; + case CHF -> Currency.CHF; + }; + } + } diff --git a/io.openems.edge.common/src/io/openems/edge/common/currency/CurrencyConfig.java b/io.openems.edge.common/src/io/openems/edge/common/currency/CurrencyConfig.java deleted file mode 100644 index 2448ff5b004..00000000000 --- a/io.openems.edge.common/src/io/openems/edge/common/currency/CurrencyConfig.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.openems.edge.common.currency; - -import io.openems.edge.common.meta.Meta; -import io.openems.edge.common.meta.Meta.ChannelId; - -/** - * The {@link ChannelId#CURRENCY} mandates the selection of the 'currency' - * configuration property of this specific type. Subsequently, this selected - * property is transformed into the corresponding {@link Currency} type before - * being written through {@link Meta#_setCurrency(Currency)}. - */ -public enum CurrencyConfig { - /** - * Euro. - */ - EUR, - /** - * Swedish Krona. - */ - SEK, - /** - * Swiss Francs. - */ - CHF; - - /** - * Converts the {@link CurrencyConfig} to the {@link Currency}. - * - * @return The {@link Currency}. - */ - public Currency toCurrency() { - return switch (this) { - case EUR -> Currency.EUR; - case SEK -> Currency.SEK; - case CHF -> Currency.CHF; - }; - } -} diff --git a/io.openems.edge.common/src/io/openems/edge/common/host/Host.java b/io.openems.edge.common/src/io/openems/edge/common/host/Host.java index 27366c7d401..85a4b328ec2 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/host/Host.java +++ b/io.openems.edge.common/src/io/openems/edge/common/host/Host.java @@ -17,6 +17,15 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { DISK_IS_FULL(Doc.of(Level.INFO) // .text("Disk is full")), // HOSTNAME(Doc.of(OpenemsType.STRING)), // + + /** + * Operating System Version. + * + *

+ * e. g. 'Raspbian GNU/Linux 11 (bullseye)' or 'Windows 11' + */ + OS_VERSION(Doc.of(OpenemsType.STRING) // + .text("Operating system version")), // ; private final Doc doc; @@ -69,7 +78,7 @@ public default StringReadChannel getHostnameChannel() { } /** - * Gets the Disk is Full Warning State. See {@link ChannelId#HOSTNAME}. + * Gets the hostname. See {@link ChannelId#HOSTNAME}. * * @return the Channel {@link Value} */ @@ -86,4 +95,32 @@ public default void _setHostname(String value) { this.getHostnameChannel().setNextValue(value); } + /** + * Gets the Channel for {@link ChannelId#OS_VERSION}. + * + * @return the Channel + */ + public default StringReadChannel getOsVersionChannel() { + return this.channel(ChannelId.OS_VERSION); + } + + /** + * Gets the operating system version. See {@link ChannelId#OS_VERSION}. + * + * @return the Channel {@link Value} + */ + public default Value getOsVersion() { + return this.getOsVersionChannel().value(); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#OS_VERSION} + * Channel. + * + * @param value the next value + */ + public default void _setOsVersion(String value) { + this.getOsVersionChannel().setNextValue(value); + } + } diff --git a/io.openems.edge.common/src/io/openems/edge/common/meta/Meta.java b/io.openems.edge.common/src/io/openems/edge/common/meta/Meta.java index c10ee0d08f2..aa31e104d1f 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/meta/Meta.java +++ b/io.openems.edge.common/src/io/openems/edge/common/meta/Meta.java @@ -3,6 +3,7 @@ import io.openems.common.OpenemsConstants; import io.openems.common.channel.AccessMode; import io.openems.common.channel.PersistencePriority; +import io.openems.common.channel.Unit; import io.openems.common.oem.OpenemsEdgeOem; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; @@ -12,6 +13,7 @@ import io.openems.edge.common.modbusslave.ModbusSlave; import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; import io.openems.edge.common.modbusslave.ModbusSlaveTable; +import io.openems.edge.common.modbusslave.ModbusType; public interface Meta extends ModbusSlave { @@ -29,7 +31,18 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ VERSION(Doc.of(OpenemsType.STRING) // .persistencePriority(PersistencePriority.HIGH)), - + /** + * System Time: seconds since 1st January 1970 00:00:00 UTC. + * + *

    + *
  • Interface: Meta + *
  • Type: Long + *
+ */ + SYSTEM_TIME_UTC(Doc.of(OpenemsType.LONG) // + .unit(Unit.SECONDS) // + .text("System Time: seconds since 1st January 1970 00:00:00 UTC") // + .persistencePriority(PersistencePriority.VERY_LOW)), /** * Edge currency. * @@ -73,6 +86,7 @@ public static ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode, Openem .string16(51, "Manufacturer Version", oem.getManufacturerVersion()) // .string16(67, "Manufacturer Serial Number", oem.getManufacturerSerialNumber()) // .string16(83, "Manufacturer EMS Serial Number", oem.getManufacturerEmsSerialNumber()) // + .channel(99, ChannelId.SYSTEM_TIME_UTC, ModbusType.UINT64) // .build()); } diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordChannel.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordChannel.java index 670a58a3ebc..0a8f7239042 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordChannel.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordChannel.java @@ -41,6 +41,7 @@ public ModbusRecordChannel(int offset, ModbusType type, ChannelId channelId, Acc case STRING16 -> ModbusRecordString16.BYTE_LENGTH; case ENUM16, UINT16 -> ModbusRecordUint16.BYTE_LENGTH; case UINT32 -> ModbusRecordUint32.BYTE_LENGTH; + case UINT64 -> ModbusRecordUint64.BYTE_LENGTH; }; this.writeValueBuffer = new Byte[byteLength]; } @@ -140,6 +141,11 @@ public byte[] getValue(OpenemsComponent component) { case READ_ONLY, READ_WRITE -> ModbusRecordUint32.toByteArray(value); case WRITE_ONLY -> ModbusRecordUint32.UNDEFINED_VALUE; }; + case UINT64 -> // + switch (this.accessMode) { + case READ_ONLY, READ_WRITE -> ModbusRecordUint64.toByteArray(value); + case WRITE_ONLY -> ModbusRecordUint64.UNDEFINED_VALUE; + }; }; } @@ -190,6 +196,7 @@ public void writeValue(int index, byte byte1, byte byte2) { case STRING16 -> ""; // TODO implement String conversion case ENUM16, UINT16 -> buff.getShort(); case UINT32 -> buff.getInt(); + case UINT64 -> buff.getLong(); }; // Forward Value to ApiWorker diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordConstant.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordConstant.java index 7158dd50e88..9a5b376f6c3 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordConstant.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordConstant.java @@ -1,5 +1,8 @@ package io.openems.edge.common.modbusslave; +import java.util.function.Consumer; +import java.util.function.Function; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,4 +52,44 @@ public AccessMode getAccessMode() { return AccessMode.READ_ONLY; } + /** + * Generates a common toString() method for implementations of + * {@link ModbusRecordConstant}. + * + * @param the type of the value + * @param name the name of the implementation class + * @param callback a {@link StringBuilder} callback + * @param value the actual value + * @param toHexString the toHexString() method + * @return a {@link String} + */ + protected String generateToString(String name, Consumer callback, T value, + Function toHexString) { + var b = new StringBuilder() // + .append(name) // + .append(" ["); + if (callback != null) { + callback.accept(b); + } + b.append("value="); + if (value != null) { + b.append(value); + if (toHexString != null) { + b.append("/0x").append(toHexString.apply(value)); + } + } else { + b.append("UNDEFINED"); + } + return b.append(", type=").append(this.getType()) // + .append("]") // + .toString(); + } + + protected String generateToString(String name, T value, Function toHexString) { + return this.generateToString(name, null, value, toHexString); + } + + protected String generateToString(String name, T value) { + return this.generateToString(name, null, value, null); + } } diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordCycleValue.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordCycleValue.java index 11a846349de..47cfe30253d 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordCycleValue.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordCycleValue.java @@ -64,21 +64,14 @@ public void updateValue(T component) { @Override public byte[] getValue(OpenemsComponent component) { - switch (this.getType()) { - case FLOAT32: - return ModbusRecordFloat32.toByteArray(this.value); - case FLOAT64: - return ModbusRecordFloat64.toByteArray(this.value); - case STRING16: - return ModbusRecordString16.toByteArray(this.value); - case ENUM16: - case UINT16: - return ModbusRecordUint16.toByteArray(this.value); - case UINT32: - return ModbusRecordUint32.toByteArray(this.value); - } - assert true; - return new byte[0]; + return switch (this.getType()) { + case FLOAT32 -> ModbusRecordFloat32.toByteArray(this.value); + case FLOAT64 -> ModbusRecordFloat64.toByteArray(this.value); + case STRING16 -> ModbusRecordString16.toByteArray(this.value); + case ENUM16, UINT16 -> ModbusRecordUint16.toByteArray(this.value); + case UINT32 -> ModbusRecordUint32.toByteArray(this.value); + case UINT64 -> ModbusRecordUint64.toByteArray(this.value); + }; } @Override diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordFloat32.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordFloat32.java index 591bfbd7d9f..61cdce90776 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordFloat32.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordFloat32.java @@ -20,7 +20,7 @@ public ModbusRecordFloat32(int offset, String name, Float value) { @Override public String toString() { - return "ModbusRecordFloat32 [value=" + this.value + ", type=" + this.getType() + "]"; + return this.generateToString("ModbusRecordFloat32", this.value); } /** diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordFloat64.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordFloat64.java index 6cb32c747cf..34613a120c0 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordFloat64.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordFloat64.java @@ -21,7 +21,7 @@ public ModbusRecordFloat64(int offset, String name, Double value) { @Override public String toString() { - return "ModbusRecordFloat64 [value=" + this.value + ", type=" + this.getType() + "]"; + return this.generateToString("ModbusRecordFloat64", this.value); } /** diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordString16.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordString16.java index 532e9e07c28..f5cdfb6466b 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordString16.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordString16.java @@ -20,7 +20,7 @@ public ModbusRecordString16(int offset, String name, String value) { @Override public String toString() { - return "ModbusRecordString16 [value=" + this.value + ", type=" + this.getType() + "]"; + return this.generateToString("ModbusRecordString16", this.value); } /** @@ -30,6 +30,9 @@ public String toString() { * @return the byte array */ public static byte[] toByteArray(String value) { + if (value == null) { + return UNDEFINED_VALUE; + } var result = new byte[BYTE_LENGTH]; var converted = value.getBytes(StandardCharsets.US_ASCII); System.arraycopy(converted, 0, result, 0, Math.min(BYTE_LENGTH, converted.length)); diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16.java index dc92046984c..3e2e751cde0 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16.java @@ -20,8 +20,7 @@ public ModbusRecordUint16(int offset, String name, Short value) { @Override public String toString() { - return "ModbusRecordUInt16 [value=" + this.value + "/0x" + Integer.toHexString(this.value) + ", type=" - + this.getType() + "]"; + return generateToString("ModbusRecordUInt16", this.value, v -> Integer.toHexString(v)); } /** diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16BlockLength.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16BlockLength.java index 03fb4a11ecf..e66ceb2641e 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16BlockLength.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16BlockLength.java @@ -11,8 +11,9 @@ public ModbusRecordUint16BlockLength(int offset, String blockName, short length) @Override public String toString() { - return "ModbusRecordUint16BlockLength [blockName=" + this.blockName + ", value=" + this.value + "/0x" - + Integer.toHexString(this.value) + ", type=" + this.getType() + "]"; + return generateToString("ModbusRecordUint16BlockLength", + b -> b.append("blockName=").append(this.blockName).append(", "), this.value, + v -> Integer.toHexString(v)); } } diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16Hash.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16Hash.java index 14780d4fc35..e1e093c1280 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16Hash.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint16Hash.java @@ -11,8 +11,8 @@ public ModbusRecordUint16Hash(int offset, String text) { @Override public String toString() { - return "ModbusRecordUint16Hash [text=" + this.text + ", value=" + this.value + "/0x" - + Integer.toHexString(this.value) + ", type=" + this.getType() + "]"; + return generateToString("ModbusRecordUint16Hash", b -> b.append("text=").append(this.text).append(", "), + this.value, v -> Integer.toHexString(v & 0xffff)); } @Override diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint32.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint32.java index 0e0a9b05e4a..23bae440c32 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint32.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint32.java @@ -20,8 +20,7 @@ public ModbusRecordUint32(int offset, String name, Integer value) { @Override public String toString() { - return "ModbusRecordUInt32 [value=" + this.value + "/0x" + Integer.toHexString(this.value) + ", type=" - + this.getType() + "]"; + return generateToString("ModbusRecordUInt32", this.value, Integer::toHexString); } /** diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint64.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint64.java new file mode 100644 index 00000000000..87ccc8e779d --- /dev/null +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint64.java @@ -0,0 +1,59 @@ +package io.openems.edge.common.modbusslave; + +import java.nio.ByteBuffer; + +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.type.TypeUtils; + +public class ModbusRecordUint64 extends ModbusRecordConstant { + + public static final byte[] UNDEFINED_VALUE = { // + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, // + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF }; + + public static final int BYTE_LENGTH = 8; + + protected final Long value; + + public ModbusRecordUint64(int offset, String name, Long value) { + super(offset, name, ModbusType.UINT64, toByteArray(value)); + this.value = value; + } + + @Override + public String toString() { + return this.generateToString("ModbusRecordUInt64", this.value, Long::toHexString); + } + + /** + * Convert to byte array. + * + * @param value the value + * @return the byte array + */ + public static byte[] toByteArray(long value) { + return ByteBuffer.allocate(BYTE_LENGTH).putLong(value).array(); + } + + /** + * Convert to byte array. + * + * @param value the value + * @return the byte array + */ + public static byte[] toByteArray(Object value) { + if (value == null || value instanceof io.openems.common.types.OptionsEnum + && ((io.openems.common.types.OptionsEnum) value).isUndefined()) { + return UNDEFINED_VALUE; + } + return toByteArray((long) TypeUtils.getAsType(OpenemsType.LONG, value)); + } + + @Override + public String getValueDescription() { + return this.value != null // + ? "\"" + Long.toString(this.value) + "\"" // + : ""; + } + +} diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint64Reserved.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint64Reserved.java new file mode 100644 index 00000000000..d6018217403 --- /dev/null +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusRecordUint64Reserved.java @@ -0,0 +1,14 @@ +package io.openems.edge.common.modbusslave; + +public class ModbusRecordUint64Reserved extends ModbusRecordUint64 { + + public ModbusRecordUint64Reserved(int offset) { + super(offset, "Reserved", null); + } + + @Override + public String toString() { + return "ModbusRecordUint64Reserved [type=" + this.getType() + "]"; + } + +} diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusSlaveNatureTable.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusSlaveNatureTable.java index da1d57c03af..f358ad8511b 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusSlaveNatureTable.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusSlaveNatureTable.java @@ -63,22 +63,12 @@ public Builder channel(int offset, ChannelId channelId, ModbusType type) { } else { // Channel did not pass filter -> show as Reserved switch (type) { - case FLOAT32: - this.float32Reserved(offset); - break; - case FLOAT64: - this.float64Reserved(offset); - break; - case STRING16: - this.string16Reserved(offset); - break; - case ENUM16: - case UINT16: - this.uint16Reserved(offset); - break; - case UINT32: - this.uint32Reserved(offset); - break; + case FLOAT32 -> this.float32Reserved(offset); + case FLOAT64 -> this.float64Reserved(offset); + case STRING16 -> this.string16Reserved(offset); + case ENUM16, UINT16 -> this.uint16Reserved(offset); + case UINT32 -> this.uint32Reserved(offset); + case UINT64 -> this.uint64Reserved(offset); } } return this; @@ -158,6 +148,18 @@ public Builder uint32Reserved(int offset) { return this; } + /** + * Add a Unsigned Int 64 Reserved value to the {@link ModbusSlaveNatureTable} + * {@link Builder}. + * + * @param offset the address offset + * @return myself + */ + public Builder uint64Reserved(int offset) { + this.add(new ModbusRecordUint64Reserved(offset)); + return this; + } + /** * Add a Float 32 value to the {@link ModbusSlaveNatureTable} {@link Builder}. * diff --git a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusType.java b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusType.java index 5da35991871..df071d14fdc 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusType.java +++ b/io.openems.edge.common/src/io/openems/edge/common/modbusslave/ModbusType.java @@ -4,6 +4,7 @@ public enum ModbusType { ENUM16(1, "enum16"), // UINT16(1, "uint16"), // UINT32(2, "uint32"), // + UINT64(4, "uint64"), // FLOAT32(2, "float32"), // FLOAT64(4, "float64"), // STRING16(16, "string16"); diff --git a/io.openems.edge.common/src/io/openems/edge/common/sum/DummySum.java b/io.openems.edge.common/src/io/openems/edge/common/sum/DummySum.java index 41a246d6b33..2a2b9bd7568 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/sum/DummySum.java +++ b/io.openems.edge.common/src/io/openems/edge/common/sum/DummySum.java @@ -49,4 +49,48 @@ public DummySum withGridActivePower(int value) { return this.self(); } + /** + * Set {@link Sum.ChannelId#ESS_CAPACITY}. + * + * @param value the value + * @return myself + */ + public DummySum withEssCapacity(int value) { + withValue(this, Sum.ChannelId.ESS_CAPACITY, value); + return this.self(); + } + + /** + * Set {@link Sum.ChannelId#ESS_SOC}. + * + * @param value the value + * @return myself + */ + public DummySum withEssSoc(int value) { + withValue(this, Sum.ChannelId.ESS_SOC, value); + return this.self(); + } + + /** + * Set {@link Sum.ChannelId#ESS_MIN_DISCHARGE_POWER}. + * + * @param value the value + * @return myself + */ + public DummySum withEssMinDischargePower(int value) { + withValue(this, Sum.ChannelId.ESS_MIN_DISCHARGE_POWER, value); + return this.self(); + } + + /** + * Set {@link Sum.ChannelId#ESS_MAX_DISCHARGE_POWER}. + * + * @param value the value + * @return myself + */ + public DummySum withEssMaxDischargePower(int value) { + withValue(this, Sum.ChannelId.ESS_MAX_DISCHARGE_POWER, value); + return this.self(); + } + } diff --git a/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java b/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java index c6a903df67c..b065cb50c18 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java +++ b/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java @@ -629,6 +629,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { CONSUMPTION_ACTIVE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // .persistencePriority(PersistencePriority.VERY_HIGH)), // + /** * Is there any Component Info/Warning/Fault that is getting ignored/hidden * because of the 'ignoreStateComponents' configuration setting?. @@ -716,6 +717,7 @@ public static ModbusSlaveNatureTable getModbusSlaveNatureTable(AccessMode access .channel(113, ChannelId.ESS_DISCHARGE_POWER, ModbusType.FLOAT32) // .channel(115, ChannelId.GRID_MODE, ModbusType.ENUM16) // .channel(116, ChannelId.GRID_MODE_OFF_GRID_TIME, ModbusType.FLOAT32) // + .channel(118, ChannelId.ESS_CAPACITY, ModbusType.FLOAT32) // .build(); } 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 f714fb70658..3f949bce31e 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 @@ -1,5 +1,9 @@ package io.openems.edge.common.test; +import static io.openems.common.utils.ReflectionUtils.invokeMethodViaReflection; +import static io.openems.common.utils.ReflectionUtils.invokeMethodWithoutArgumentsViaReflection; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; + import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -34,6 +38,7 @@ import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; import io.openems.common.types.OptionsEnum; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.ChannelId; import io.openems.edge.common.channel.EnumDoc; @@ -42,6 +47,11 @@ 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.sum.Sum; +import io.openems.edge.common.test.AbstractComponentTest.ChannelValue.ChannelAddressValue; +import io.openems.edge.common.test.AbstractComponentTest.ChannelValue.ChannelIdValue; +import io.openems.edge.common.test.AbstractComponentTest.ChannelValue.ChannelNameValue; +import io.openems.edge.common.test.AbstractComponentTest.ChannelValue.ComponentChannelIdValue; import io.openems.edge.common.type.TypeUtils; /** @@ -49,10 +59,49 @@ */ public abstract class AbstractComponentTest, SUT extends OpenemsComponent> { - public record ChannelValue(ChannelAddress address, Object value, boolean force) { - @Override - public String toString() { - return this.address.toString() + ":" + this.value; + public sealed interface ChannelValue { + + /** + * Gets the value. + * + * @return the value + */ + public Object value(); + + /** + * Is the value enforced?. + * + * @return true for force + */ + public boolean force(); + + public record ChannelAddressValue(ChannelAddress address, Object value, boolean force) implements ChannelValue { + @Override + public String toString() { + return this.address.toString() + ":" + this.value; + } + } + + public record ChannelIdValue(ChannelId channelId, Object value, boolean force) implements ChannelValue { + @Override + public String toString() { + return this.channelId.id() + ":" + this.value; + } + } + + public record ChannelNameValue(String channelName, Object value, boolean force) implements ChannelValue { + @Override + public String toString() { + return this.channelName + ":" + this.value; + } + } + + public record ComponentChannelIdValue(String componentId, ChannelId channelId, Object value, boolean force) + implements ChannelValue { + @Override + public String toString() { + return this.componentId + "/" + this.channelId.id() + ":" + this.value; + } } } @@ -109,19 +158,71 @@ public TestCase(String description) { } /** - * Adds an input value for a Channel. + * Adds an input value for a {@link ChannelAddress}. * * @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, false)); + this.inputs.add(new ChannelAddressValue(address, value, false)); + return this; + } + + /** + * Adds an input value for a ChannelId of the given Component. + * + * @param componentId the Component-ID + * @param channelId the Channel-ID in CamelCase + * @param value the value {@link Object} + * @return myself + */ + public TestCase input(String componentId, String channelId, Object value) { + return this.input(new ChannelAddress(componentId, channelId), value); + } + + /** + * Adds an input value for a {@link ChannelId} of the given Component. + * + * @param componentId the Component-ID + * @param channelId the {@link ChannelId} + * @param value the value {@link Object} + * @return myself + */ + public TestCase input(String componentId, ChannelId channelId, Object value) { + this.inputs.add(new ComponentChannelIdValue(componentId, channelId, value, false)); + return this; + } + + /** + * Adds an input value for a {@link ChannelId} of the system-under-test. + * + * @param channelId the {@link ChannelId} + * @param value the value {@link Object} + * @return myself + */ + public TestCase input(ChannelId channelId, Object value) { + if (channelId instanceof Sum.ChannelId) { + return this.input("_sum", channelId, value); + } + this.inputs.add(new ChannelIdValue(channelId, value, false)); + return this; + } + + /** + * Adds an input value for a ChannelId of the system-under-test. + * + * @param channelName the Channel + * @param value the value {@link Object} + * @return myself + */ + public TestCase input(String channelName, Object value) { + this.inputs.add(new ChannelNameValue(channelName, value, false)); return this; } /** - * Enforces an input value for a Channel. + * Enforces an input value for a {@link ChannelAddress}. * *

* Use this method if you want to be sure, that the Channel actually applies the @@ -132,19 +233,130 @@ public TestCase input(ChannelAddress address, Object value) { * @return myself */ public TestCase inputForce(ChannelAddress address, Object value) { - this.inputs.add(new ChannelValue(address, value, true)); + this.inputs.add(new ChannelAddressValue(address, value, true)); + return this; + } + + /** + * Enforces an input value for a {@link ChannelAddress}. + * + *

+ * Use this method if you want to be sure, that the Channel actually applies the + * value, e.g. to override a {@link Debounce} setting. + * + * @param componentId the Component-ID + * @param channelId the Channel-ID + * @param value the value {@link Object} + * @return myself + */ + public TestCase inputForce(String componentId, String channelId, Object value) { + return this.inputForce(new ChannelAddress(componentId, channelId), value); + } + + /** + * Enforces an input value for a {@link ChannelId} of the given Component. + * + * @param componentId the Component-ID + * @param channelId the {@link ChannelId} + * @param value the value {@link Object} + * @return myself + */ + public TestCase inputForce(String componentId, ChannelId channelId, Object value) { + this.inputs.add(new ComponentChannelIdValue(componentId, channelId, value, true)); return this; } /** - * Adds an expected output value for a Channel. + * Enforces an input value for a {@link ChannelId} of the system-under-test. + * + *

+ * Use this method if you want to be sure, that the Channel actually applies the + * value, e.g. to override a {@link Debounce} setting. + * + * @param channelId the {@link ChannelId} + * @param value the value {@link Object} + * @return myself + */ + public TestCase inputForce(ChannelId channelId, Object value) { + this.inputs.add(new ChannelIdValue(channelId, value, true)); + return this; + } + + /** + * Enforces an input value for a Channel of the system-under-test. + * + *

+ * Use this method if you want to be sure, that the Channel actually applies the + * value, e.g. to override a {@link Debounce} setting. + * + * @param channelName the Channel + * @param value the value {@link Object} + * @return myself + */ + public TestCase inputForce(String channelName, Object value) { + this.inputs.add(new ChannelNameValue(channelName, value, true)); + return this; + } + + /** + * Adds an expected output value for a {@link ChannelAddress}. * * @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, false)); + this.outputs.add(new ChannelAddressValue(address, value, false)); + return this; + } + + /** + * Adds an expected output value for a {@link ChannelAddress}. + * + * @param componentId the Component-ID + * @param channelId the Channel-ID in CamelCase + * @param value the value {@link Object} + * @return myself + */ + public TestCase output(String componentId, String channelId, Object value) { + return this.output(new ChannelAddress(componentId, channelId), value); + } + + /** + * Adds an expected output value for a {@link ChannelId} of the given Component. + * + * @param componentId the Component-ID + * @param channelId the {@link ChannelId} + * @param value the value {@link Object} + * @return myself + */ + public TestCase output(String componentId, ChannelId channelId, Object value) { + this.outputs.add(new ComponentChannelIdValue(componentId, channelId, value, true)); + return this; + } + + /** + * Adds an expected output value for a {@link ChannelId} of the + * system-under-test. + * + * @param channelId the {@link ChannelId} + * @param value the value {@link Object} + * @return myself + */ + public TestCase output(ChannelId channelId, Object value) { + this.outputs.add(new ChannelIdValue(channelId, value, false)); + return this; + } + + /** + * Adds an expected output value for a Channel of the system-under-test. + * + * @param channelName the Channel + * @param value the value {@link Object} + * @return myself + */ + public TestCase output(String channelName, Object value) { + this.outputs.add(new ChannelNameValue(channelName, value, false)); return this; } @@ -286,20 +498,14 @@ public void applyTimeLeap() { /** * Applies the values for input channels. * - * @param components Referenced components + * @param act the {@link AbstractComponentTest} * @throws OpenemsNamedException on error * @throws IllegalArgumentException on error */ - protected void applyInputs(Map components) + protected void applyInputs(AbstractComponentTest act) throws IllegalArgumentException, OpenemsNamedException { for (var input : this.inputs) { - var component = components.get(input.address.getComponentId()); - if (component == null) { - throw new IllegalArgumentException("On TestCase [" + this.description + "]: " // - + "the component [" + input.address.getComponentId() + "] " // - + "was not added to the OpenEMS Component test framework!"); - } - var channel = component.channel(input.address.getChannelId()); + final Channel channel = this.getChannel(act, input); // (Force) set the Read-Value do { @@ -317,38 +523,76 @@ protected void applyInputs(Map components) /** * Validates the output values. * - * @param components Referenced components + * @param act the {@link AbstractComponentTest} * @throws Exception on validation failure */ - protected void validateOutputs(Map components) throws Exception { + @SuppressWarnings("unchecked") + protected void validateOutputs(AbstractComponentTest act) throws Exception { for (var output : this.outputs) { - var expected = output.value; - var channel = components.get(output.address.getComponentId()).channel(output.address.getChannelId()); + final Channel channel = this.getChannel(act, output); + Object got; - if (channel instanceof WriteChannel) { - got = ((WriteChannel) channel).getNextWriteValueAndReset().orElse(null); + if (channel instanceof WriteChannel wc) { + got = wc.getNextWriteValueAndReset().orElse(null); } else { var value = channel.getNextValue(); got = value.orElse(null); } - // Try to parse an Enum if (channel.channelDoc() instanceof EnumDoc) { var enumDoc = (EnumDoc) channel.channelDoc(); var intGot = TypeUtils.getAsType(OpenemsType.INTEGER, got); got = enumDoc.getOption(intGot); } - if (!Objects.equals(expected, got)) { + if (!Objects.equals(output.value(), got)) { throw new Exception("On TestCase [" + this.description + "]: " // - + "expected [" + output.value + "] " // + + "expected [" + output.value() + "] " // + "got [" + got + "] " // - + "for Channel [" + output.address.toString() + "] " // + + "for Channel [" + output.toString() + "] " // + "on Inputs [" + this.inputs + "]"); } } } + + private OpenemsComponent getComponent(Map components, String componentId) { + var component = components.get(componentId); + if (component != null) { + return component; + } + throw new IllegalArgumentException("On TestCase [" + this.description + "]: " // + + "the component [" + componentId + "] " // + + "was not added to the OpenEMS Component test framework!"); + } + + private Channel getChannel(AbstractComponentTest act, ChannelValue cv) + throws IllegalArgumentException { + if (cv instanceof ChannelAddressValue cav) { + var component = this.getComponent(act.components, cav.address.getComponentId()); + return component.channel(cav.address.getChannelId()); + } + + if (cv instanceof ChannelIdValue civ) { + return act.sut.channel(civ.channelId); + } + + if (cv instanceof ChannelNameValue civ2) { + return act.sut.channel(civ2.channelName); + } + + if (cv instanceof ComponentChannelIdValue cciv) { + var component = this.getComponent(act.components, cciv.componentId()); + return component.channel(cciv.channelId()); + } + + throw new IllegalArgumentException("Unhandled subtype of ChannelValue"); + } } + /** + * The {@link OpenemsComponent} to be tested. "sut" is for system-under-test. + */ + public final SUT sut; + /** * References added by {@link #addReference()}. */ @@ -359,11 +603,6 @@ protected void validateOutputs(Map components) throws */ private final Map components = new HashMap<>(); - /** - * The {@link OpenemsComponent} to be tested. "sut" is for system-under-test. - */ - private final SUT sut; - /** * Constructs the Component-Test and validates the implemented Channel-IDs. * @@ -469,11 +708,9 @@ public SELF addReference(String memberName, Object object) throws Exception { private boolean addReference(Class clazz, String memberName, Object object) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { try { - var field = clazz.getDeclaredField(memberName); - field.setAccessible(true); - field.set(this.sut, object); + setAttributeViaReflection(this.sut, memberName, object); return true; - } catch (NoSuchFieldException e) { + } catch (ReflectionException e) { // Ignore. Try method. if (this.invokeSingleArgMethod(clazz, memberName, object)) { return true; @@ -603,8 +840,7 @@ private boolean callActivateOrModified(String methodName, AbstractComponentConfi } args[i] = arg; } - method.setAccessible(true); - method.invoke(this.sut, args); + invokeMethodViaReflection(this.sut, method, args); return true; } return false; @@ -624,14 +860,10 @@ private void callModified(AbstractComponentConfig config) throws Exception { private void callDeactivate() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { - Class clazz = this.sut.getClass(); - var method = clazz.getDeclaredMethod("deactivate"); - method.setAccessible(true); - method.invoke(this.sut); + invokeMethodWithoutArgumentsViaReflection(this.sut, "deactivate"); } - private boolean invokeSingleArgMethod(Class clazz, String methodName, Object arg) - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + private boolean invokeSingleArgMethod(Class clazz, String methodName, Object arg) throws ReflectionException { var methods = clazz.getDeclaredMethods(); for (Method method : methods) { if (!method.getName().equals(methodName)) { @@ -646,8 +878,7 @@ private boolean invokeSingleArgMethod(Class clazz, String methodName, Object continue; } - method.setAccessible(true); - method.invoke(this.sut, arg); + invokeMethodViaReflection(this.sut, method, arg); return true; } @@ -670,7 +901,7 @@ public SELF next(TestCase testCase) throws Exception { for (Channel channel : this.getSut().channels()) { channel.nextProcessImage(); } - testCase.applyInputs(this.components); + testCase.applyInputs(this); this.onAfterProcessImage(); executeCallbacks(testCase.onAfterProcessImageCallbacks); this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE); @@ -691,7 +922,7 @@ public SELF next(TestCase testCase) throws Exception { this.onAfterWrite(); executeCallbacks(testCase.onAfterWriteCallbacks); this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE); - testCase.validateOutputs(this.components); + testCase.validateOutputs(this); return this.self(); } @@ -725,9 +956,11 @@ private static void executeCallbacks(List> callbacks * */ protected void handleEvent(String topic) throws Exception { - if (this.sut instanceof EventHandler) { - var event = new Event(topic, new HashMap()); - ((EventHandler) this.sut).handleEvent(event); + var event = new Event(topic, new HashMap()); + for (var component : this.components.values()) { + if (component instanceof EventHandler eh) { + eh.handleEvent(event); + } } } @@ -802,7 +1035,5 @@ protected void onExecuteWrite() throws OpenemsNamedException { * @throws OpenemsNamedException on error */ 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 a3a43bbe07f..db5ebb96825 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 @@ -1,5 +1,7 @@ package io.openems.edge.common.test; +import static io.openems.edge.common.test.TestUtils.createDummyClock; + import java.io.IOException; import java.time.Clock; import java.util.ArrayList; @@ -47,7 +49,7 @@ public class DummyComponentManager implements ComponentManager, ComponentJsonApi private ConfigurationAdmin configurationAdmin = null; public DummyComponentManager() { - this(Clock.systemDefaultZone()); + this(createDummyClock()); } public DummyComponentManager(Clock clock) { diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/TestUtils.java b/io.openems.edge.common/src/io/openems/edge/common/test/TestUtils.java index d58c7dbf484..4cb54a3f37e 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/test/TestUtils.java +++ b/io.openems.edge.common/src/io/openems/edge/common/test/TestUtils.java @@ -2,9 +2,14 @@ import java.io.IOException; import java.net.ServerSocket; +import java.time.Instant; +import java.util.function.BiFunction; +import java.util.function.Function; +import io.openems.common.test.TimeLeapClock; 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.OpenemsComponent; public class TestUtils { @@ -13,6 +18,15 @@ private TestUtils() { } + /** + * Creates a {@link TimeLeapClock} for 1st January 2000 00:00. + * + * @return the {@link TimeLeapClock} + */ + public static TimeLeapClock createDummyClock() { + return new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */); + } + /** * Finds and returns an open port. * @@ -69,4 +83,24 @@ public static void withValue(Channel channel, Object value) { channel.setNextValue(value); channel.nextProcessImage(); } + + /** + * Helper to test a {@link #withValue(Channel, Object)} method in a JUnit test. + * + * @param the type of the {@link AbstractDummyOpenemsComponent} + * @param sut the actual system-under-test + * @param setter the getChannel getter method + * @param getter the withChannel setter method + */ + public static void testWithValue(T sut, BiFunction setter, Function> getter) { + var before = getter.apply(sut).get(); + if (before != null) { + throw new IllegalArgumentException("TestUtils.testWithValue() expected [null] got [" + before + "]"); + } + setter.apply(sut, 123); + var after = getter.apply(sut).get().intValue(); + if (after != 123) { + throw new IllegalArgumentException("TestUtils.testWithValue() expected [123] got [" + after + "]"); + } + } } 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 babada99a81..d43a8bc6716 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 @@ -364,23 +364,15 @@ public static JsonElement getAsJson(OpenemsType type, Object originalValue) { return JsonNull.INSTANCE; } var value = TypeUtils.getAsType(type, originalValue); - switch (type) { - case BOOLEAN: - return new JsonPrimitive((Boolean) value ? 1 : 0); - case SHORT: - return new JsonPrimitive((Short) value); - case INTEGER: - return new JsonPrimitive((Integer) value); - case LONG: - return new JsonPrimitive((Long) value); - case FLOAT: - return new JsonPrimitive((Float) value); - case DOUBLE: - return new JsonPrimitive((Double) value); - case STRING: - return new JsonPrimitive((String) value); - } - throw new IllegalArgumentException("Converter for value [" + value + "] to JSON is not implemented."); + return switch (type) { + case BOOLEAN -> new JsonPrimitive((Boolean) value ? 1 : 0); + case SHORT -> new JsonPrimitive((Short) value); + case INTEGER -> new JsonPrimitive((Integer) value); + case LONG -> new JsonPrimitive((Long) value); + case FLOAT -> new JsonPrimitive((Float) value); + case DOUBLE -> new JsonPrimitive((Double) value); + case STRING -> new JsonPrimitive((String) value); + }; } /** @@ -427,6 +419,28 @@ public static Integer sum(Integer... values) { return result; } + /** + * Safely add Floats. If one of them is null it is considered '0'. If all of + * them are null, 'null' is returned. + * + * @param values the {@link Float} values + * @return the sum + */ + public static Float sum(Float... values) { + Float result = null; + for (Float value : values) { + if (value == null) { + continue; + } + if (result == null) { + result = value; + } else { + result += value; + } + } + return result; + } + /** * Safely add Longs. If one of them is null it is considered '0'. If all of them * are null, 'null' is returned. diff --git a/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordFloat32Test.java b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordFloat32Test.java new file mode 100644 index 00000000000..197fbff8dcf --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordFloat32Test.java @@ -0,0 +1,30 @@ +package io.openems.edge.common.modbusslave; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ModbusRecordFloat32Test { + + @Test + public void testValue() { + var sut = new ModbusRecordFloat32(0, "foo", 1234567.89F); + assertEquals("ModbusRecordFloat32 [value=1234567.9, type=float32]", sut.toString()); + assertEquals("\"1234567.9\"", sut.getValueDescription()); + } + + @Test + public void testNull() { + var sut = new ModbusRecordFloat32(0, "bar", null); + assertEquals("ModbusRecordFloat32 [value=UNDEFINED, type=float32]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + + @Test + public void testReserved() { + var sut = new ModbusRecordFloat32Reserved(0); + assertEquals("ModbusRecordFloat32Reserved [type=float32]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + +} diff --git a/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordFloat64Test.java b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordFloat64Test.java new file mode 100644 index 00000000000..70a21900022 --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordFloat64Test.java @@ -0,0 +1,30 @@ +package io.openems.edge.common.modbusslave; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ModbusRecordFloat64Test { + + @Test + public void testValue() { + var sut = new ModbusRecordFloat64(0, "foo", 1234567.89); + assertEquals("ModbusRecordFloat64 [value=1234567.89, type=float64]", sut.toString()); + assertEquals("\"1234567.89\"", sut.getValueDescription()); + } + + @Test + public void testNull() { + var sut = new ModbusRecordFloat64(0, "bar", null); + assertEquals("ModbusRecordFloat64 [value=UNDEFINED, type=float64]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + + @Test + public void testReserved() { + var sut = new ModbusRecordFloat64Reserved(0); + assertEquals("ModbusRecordFloat64Reserved [type=float64]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + +} diff --git a/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordString16Test.java b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordString16Test.java new file mode 100644 index 00000000000..d6bea0edd02 --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordString16Test.java @@ -0,0 +1,42 @@ +package io.openems.edge.common.modbusslave; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; + +public class ModbusRecordString16Test { + + @Test + public void testValue() { + var sut = new ModbusRecordString16(0, "foo", "bar"); + assertEquals("ModbusRecordString16 [value=bar, type=string16]", sut.toString()); + assertEquals("\"bar\"", sut.getValueDescription()); + } + + @Test + public void testNull() { + var sut = new ModbusRecordString16(0, "bar", null); + assertEquals("ModbusRecordString16 [value=UNDEFINED, type=string16]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + + @Test + public void testReserved() { + var sut = new ModbusRecordString16Reserved(0); + assertEquals("ModbusRecordString16Reserved [type=string16]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + + @Test + public void testToByteArray() { + assertEquals("[72, 101, 108, 108, 111, "// + + "32, " // + + "87, 111, 114, 108, 100, " // + + "0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]", + Arrays.toString(ModbusRecordString16.toByteArray((Object) "Hello World"))); + assertEquals("[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]", + Arrays.toString(ModbusRecordString16.toByteArray((Object) null))); + } +} diff --git a/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordUint16Test.java b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordUint16Test.java new file mode 100644 index 00000000000..7752f124cd2 --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordUint16Test.java @@ -0,0 +1,52 @@ +package io.openems.edge.common.modbusslave; + +import static io.openems.common.test.DummyOptionsEnum.UNDEFINED; +import static io.openems.common.test.DummyOptionsEnum.VALUE_1; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; + +public class ModbusRecordUint16Test { + + @Test + public void testValue() { + var sut = new ModbusRecordUint16(0, "foo", (short) 12345); + assertEquals("ModbusRecordUInt16 [value=12345/0x3039, type=uint16]", sut.toString()); + assertEquals("\"12345\"", sut.getValueDescription()); + } + + @Test + public void testNull() { + var sut = new ModbusRecordUint16(0, "bar", null); + assertEquals("ModbusRecordUInt16 [value=UNDEFINED, type=uint16]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + + @Test + public void testOptionsEnum() { + assertEquals("[-1, -1]", Arrays.toString(ModbusRecordUint16.toByteArray(UNDEFINED))); + assertEquals("[0, 1]", Arrays.toString(ModbusRecordUint16.toByteArray(VALUE_1))); + } + + @Test + public void testReserved() { + var sut = new ModbusRecordUint16Reserved(0); + assertEquals("ModbusRecordUint16Reserved [type=uint16]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + + @Test + public void testBlockLength() { + assertEquals("ModbusRecordUint16BlockLength [blockName=block, value=12345/0x3039, type=uint16]", + new ModbusRecordUint16BlockLength(0, "block", (short) 12345).toString()); + } + + @Test + public void testHash() { + var sut = new ModbusRecordUint16Hash(0, "hash"); + assertEquals("ModbusRecordUint16Hash [text=hash, value=-16114/0xc10e, type=uint16]", sut.toString()); + assertEquals("\"0xc10e\"", sut.getValueDescription()); + } +} diff --git a/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordUint32Test.java b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordUint32Test.java new file mode 100644 index 00000000000..0f5504bf857 --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordUint32Test.java @@ -0,0 +1,40 @@ +package io.openems.edge.common.modbusslave; + +import static io.openems.common.test.DummyOptionsEnum.UNDEFINED; +import static io.openems.common.test.DummyOptionsEnum.VALUE_1; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; + +public class ModbusRecordUint32Test { + + @Test + public void testValue() { + var sut = new ModbusRecordUint32(0, "foo", 123456789); + assertEquals("ModbusRecordUInt32 [value=123456789/0x75bcd15, type=uint32]", sut.toString()); + assertEquals("\"123456789\"", sut.getValueDescription()); + } + + @Test + public void testNull() { + var sut = new ModbusRecordUint32(0, "bar", null); + assertEquals("ModbusRecordUInt32 [value=UNDEFINED, type=uint32]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + + @Test + public void testOptionsEnum() { + assertEquals("[-1, -1, -1, -1]", Arrays.toString(ModbusRecordUint32.toByteArray(UNDEFINED))); + assertEquals("[0, 0, 0, 1]", Arrays.toString(ModbusRecordUint32.toByteArray(VALUE_1))); + } + + @Test + public void testReserved() { + var sut = new ModbusRecordUint32Reserved(0); + assertEquals("ModbusRecordUint32Reserved [type=uint32]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + +} diff --git a/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordUint64Test.java b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordUint64Test.java new file mode 100644 index 00000000000..903a5139236 --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/modbusslave/ModbusRecordUint64Test.java @@ -0,0 +1,40 @@ +package io.openems.edge.common.modbusslave; + +import static io.openems.common.test.DummyOptionsEnum.UNDEFINED; +import static io.openems.common.test.DummyOptionsEnum.VALUE_1; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; + +public class ModbusRecordUint64Test { + + @Test + public void testValue() { + var sut = new ModbusRecordUint64(0, "foo", 123456789L); + assertEquals("ModbusRecordUInt64 [value=123456789/0x75bcd15, type=uint64]", sut.toString()); + assertEquals("\"123456789\"", sut.getValueDescription()); + } + + @Test + public void testNull() { + var sut = new ModbusRecordUint64(0, "bar", null); + assertEquals("ModbusRecordUInt64 [value=UNDEFINED, type=uint64]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + + @Test + public void testOptionsEnum() { + assertEquals("[-1, -1, -1, -1, -1, -1, -1, -1]", Arrays.toString(ModbusRecordUint64.toByteArray(UNDEFINED))); + assertEquals("[0, 0, 0, 0, 0, 0, 0, 1]", Arrays.toString(ModbusRecordUint64.toByteArray(VALUE_1))); + } + + @Test + public void testReserved() { + var sut = new ModbusRecordUint64Reserved(0); + assertEquals("ModbusRecordUint64Reserved [type=uint64]", sut.toString()); + assertEquals("", sut.getValueDescription()); + } + +} diff --git a/io.openems.edge.common/test/io/openems/edge/common/sum/DummySumTest.java b/io.openems.edge.common/test/io/openems/edge/common/sum/DummySumTest.java new file mode 100644 index 00000000000..8cb89d3a6d2 --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/sum/DummySumTest.java @@ -0,0 +1,22 @@ +package io.openems.edge.common.sum; + +import static io.openems.edge.common.test.TestUtils.testWithValue; + +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsException; + +public class DummySumTest { + + @Test + public void test() throws OpenemsException { + final var sut = new DummySum(); + + testWithValue(sut, DummySum::withProductionAcActivePower, Sum::getProductionAcActivePower); + testWithValue(sut, DummySum::withGridActivePower, Sum::getGridActivePower); + testWithValue(sut, DummySum::withEssCapacity, Sum::getEssCapacity); + testWithValue(sut, DummySum::withEssSoc, Sum::getEssSoc); + testWithValue(sut, DummySum::withEssMinDischargePower, Sum::getEssMinDischargePower); + testWithValue(sut, DummySum::withEssMaxDischargePower, Sum::getEssMaxDischargePower); + } +} 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 83c76584a1c..f5ff84b47f3 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 @@ -1,5 +1,15 @@ package io.openems.edge.common.type; +import static com.google.gson.JsonNull.INSTANCE; +import static io.openems.common.types.OpenemsType.BOOLEAN; +import static io.openems.common.types.OpenemsType.DOUBLE; +import static io.openems.common.types.OpenemsType.FLOAT; +import static io.openems.common.types.OpenemsType.INTEGER; +import static io.openems.common.types.OpenemsType.LONG; +import static io.openems.common.types.OpenemsType.SHORT; +import static io.openems.common.types.OpenemsType.STRING; +import static io.openems.edge.common.type.TypeUtils.getAsJson; +import static io.openems.edge.common.type.TypeUtils.sum; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -7,8 +17,9 @@ import org.junit.Test; +import com.google.gson.JsonPrimitive; + import io.openems.common.function.ThrowingRunnable; -import io.openems.common.types.OpenemsType; import io.openems.common.types.OptionsEnum; import io.openems.edge.common.channel.value.Value; @@ -81,8 +92,8 @@ public void testMin() { @Test public void testSumDouble() { - assertNull(TypeUtils.sum((Double) null, null)); - assertEquals(4.0, TypeUtils.sum(1.5, 2.5), 0.1); + assertNull(sum((Double) null, null)); + assertEquals(4.0, sum(1.5, 2.5), 0.1); } @Test @@ -269,6 +280,47 @@ public void testGetAsType() { } } + @Test + public void testGetAsJson() { + assertEquals(INSTANCE, getAsJson(INTEGER, null)); + assertEquals(new JsonPrimitive(0), getAsJson(BOOLEAN, false)); + assertEquals(new JsonPrimitive(1), getAsJson(BOOLEAN, true)); + assertEquals(new JsonPrimitive(123), getAsJson(SHORT, 123)); + assertEquals(new JsonPrimitive(234), getAsJson(INTEGER, 234)); + assertEquals(new JsonPrimitive(345), getAsJson(LONG, 345)); + assertEquals(new JsonPrimitive(45.6F), getAsJson(FLOAT, 45.6F)); + assertEquals(new JsonPrimitive(56.7), getAsJson(DOUBLE, 56.7)); + assertEquals(new JsonPrimitive("678"), getAsJson(STRING, "678")); + } + + @Test + public void sumInteger() { + assertEquals(6, sum(1, 2, 3).intValue()); + assertNull(sum((Integer) null)); + assertEquals(6, sum(1, null, 2, 3).intValue()); + } + + @Test + public void sumFloat() { + assertEquals(6F, sum(1F, 2F, 3F).floatValue(), 0.001F); + assertNull(sum((Float) null)); + assertEquals(6F, sum(1F, null, 2F, 3F).floatValue(), 0.001F); + } + + @Test + public void sumLong() { + assertEquals(6L, sum(1L, 2L, 3L).longValue()); + assertNull(sum((Long) null)); + assertEquals(6L, sum(1L, null, 2L, 3L).longValue()); + } + + @Test + public void sumDouble() { + assertEquals(6., sum(1., 2., 3.).doubleValue(), 0.001); + assertNull(sum((Double) null)); + assertEquals(6., sum(1., null, 2., 3.).doubleValue(), 0.001); + } + private static void assertException(ThrowingRunnable runnable) { try { runnable.run(); @@ -279,31 +331,31 @@ private static void assertException(ThrowingRunnable runnable) { } private static Boolean getAsBoolean(Object value) { - return TypeUtils.getAsType(OpenemsType.BOOLEAN, value); + return TypeUtils.getAsType(BOOLEAN, value); } private static Short getAsShort(Object value) { - return TypeUtils.getAsType(OpenemsType.SHORT, value); + return TypeUtils.getAsType(SHORT, value); } private static Integer getAsInteger(Object value) { - return TypeUtils.getAsType(OpenemsType.INTEGER, value); + return TypeUtils.getAsType(INTEGER, value); } private static Long getAsLong(Object value) { - return TypeUtils.getAsType(OpenemsType.LONG, value); + return TypeUtils.getAsType(LONG, value); } private static Float getAsFloat(Object value) { - return TypeUtils.getAsType(OpenemsType.FLOAT, value); + return TypeUtils.getAsType(FLOAT, value); } private static Double getAsDouble(Object value) { - return TypeUtils.getAsType(OpenemsType.DOUBLE, value); + return TypeUtils.getAsType(DOUBLE, value); } private static String getAsString(Object value) { - return TypeUtils.getAsType(OpenemsType.STRING, value); + return TypeUtils.getAsType(STRING, value); } private static enum MyOptionsEnum implements OptionsEnum { diff --git a/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WebsocketClient.java b/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WebsocketClient.java index aeb2e73a5ba..10b7a380eab 100644 --- a/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WebsocketClient.java +++ b/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WebsocketClient.java @@ -13,6 +13,7 @@ import io.openems.common.websocket.AbstractWebsocketClient; import io.openems.common.websocket.OnClose; +import io.openems.common.websocket.WsData; public class WebsocketClient extends AbstractWebsocketClient { diff --git a/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WsData.java b/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WsData.java deleted file mode 100644 index 2dfdd355c5b..00000000000 --- a/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WsData.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.openems.edge.controller.api.backend; - -import org.java_websocket.WebSocket; - -public class WsData extends io.openems.common.websocket.WsData { - - public WsData(WebSocket ws) { - super(ws); - } - - @Override - public String toString() { - return "BackendApi.WsData []"; - } - -} diff --git a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/ControllerApiBackendImplTest.java b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/ControllerApiBackendImplTest.java index 9d594615157..d99ef35e3bf 100644 --- a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/ControllerApiBackendImplTest.java +++ b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/ControllerApiBackendImplTest.java @@ -17,8 +17,6 @@ public class ControllerApiBackendImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { @@ -44,7 +42,7 @@ public void test() throws Exception { .addReference("oem", new DummyOpenemsEdgeOem()) // .addComponent(new DummySum()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setUri("ws://localhost:" + port) // .setApikey("12345") // .setProxyType(Type.DIRECT) // diff --git a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyBackendOnRequestFactory.java b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyBackendOnRequestFactory.java index 05aed052db3..bb10ff7fd07 100644 --- a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyBackendOnRequestFactory.java +++ b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyBackendOnRequestFactory.java @@ -1,21 +1,20 @@ package io.openems.edge.controller.api.backend; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.controller.api.backend.handler.BindingRoutesJsonApiHandler; import io.openems.edge.controller.api.backend.handler.RootRequestHandler; import io.openems.edge.controller.api.common.handler.RoutesJsonApiHandler; public class DummyBackendOnRequestFactory extends BackendOnRequest.Factory { - public DummyBackendOnRequestFactory() - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyBackendOnRequestFactory() throws ReflectionException { super(); - ReflectionUtils.setAttribute(BackendOnRequest.Factory.class, this, "cso", new DummyBackendOnRequestCso()); + setAttributeViaReflection(this, "cso", new DummyBackendOnRequestCso()); } private static class DummyBackendOnRequestCso implements ComponentServiceObjects { diff --git a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyResendHistoricDataWorkerFactory.java b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyResendHistoricDataWorkerFactory.java index 91aff08460e..55e5e7f439c 100644 --- a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyResendHistoricDataWorkerFactory.java +++ b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyResendHistoricDataWorkerFactory.java @@ -1,19 +1,17 @@ package io.openems.edge.controller.api.backend; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; public class DummyResendHistoricDataWorkerFactory extends ResendHistoricDataWorkerFactory { - public DummyResendHistoricDataWorkerFactory() - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyResendHistoricDataWorkerFactory() throws ReflectionException { super(); - ReflectionUtils.setAttribute(ResendHistoricDataWorkerFactory.class, this, "cso", - new DummyResendHistoricDataWorkerCso()); + setAttributeViaReflection(this, "cso", new DummyResendHistoricDataWorkerCso()); } private static class DummyResendHistoricDataWorkerCso implements ComponentServiceObjects { diff --git a/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/handler/QueryRequestHandler.java b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/handler/QueryRequestHandler.java index 9bed397860b..64496f773cc 100644 --- a/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/handler/QueryRequestHandler.java +++ b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/handler/QueryRequestHandler.java @@ -1,11 +1,17 @@ package io.openems.edge.controller.api.common.handler; +import java.io.IOException; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.TreeSet; + import org.osgi.service.component.annotations.Component; 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.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.common.jsonrpc.request.QueryHistoricTimeseriesDataRequest; import io.openems.common.jsonrpc.request.QueryHistoricTimeseriesEnergyPerPeriodRequest; @@ -14,6 +20,13 @@ import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesDataResponse; import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesEnergyPerPeriodResponse; import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesEnergyResponse; +import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesExportXlsxResponse; +import io.openems.common.session.Language; +import io.openems.common.timedata.Resolution; +import io.openems.common.timedata.XlsxExportDetailData.XlsxExportDataEntry.HistoricTimedataSaveType; +import io.openems.common.timedata.XlsxExportUtil; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.jsonapi.EdgeKeys; import io.openems.edge.common.jsonapi.JsonApi; import io.openems.edge.common.jsonapi.JsonApiBuilder; @@ -29,6 +42,9 @@ public class QueryRequestHandler implements JsonApi { ) private volatile Timedata timedata; + @Reference + private ComponentManager componentManager; + @Override public void buildJsonApiRoutes(JsonApiBuilder builder) { builder.handleRequest(QueryHistoricTimeseriesDataRequest.METHOD, call -> { @@ -54,14 +70,39 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { request.getFromDate(), request.getToDate(), request.getChannels(), request.getResolution()); return new QueryHistoricTimeseriesEnergyPerPeriodResponse(request.getId(), data); }); - builder.handleRequest(QueryHistoricTimeseriesExportXlxsRequest.METHOD, call -> { final var request = QueryHistoricTimeseriesExportXlxsRequest.from(call.getRequest()); - return this.getTimedata().handleQueryHistoricTimeseriesExportXlxsRequest(null /* ignore Edge-ID */, request, + return this.handleQueryHistoricTimeseriesExportXlxsRequest(request, call.get(EdgeKeys.USER_KEY).getLanguage()); }); } + private QueryHistoricTimeseriesExportXlsxResponse handleQueryHistoricTimeseriesExportXlxsRequest( + QueryHistoricTimeseriesExportXlxsRequest request, Language language) throws OpenemsNamedException { + final var powerChannels = new TreeSet(QueryHistoricTimeseriesExportXlsxResponse.POWER_CHANNELS); + final var energyChannels = new TreeSet( + QueryHistoricTimeseriesExportXlsxResponse.ENERGY_CHANNELS); + final var detailData = XlsxExportUtil.getDetailData(this.componentManager.getEdgeConfig()); + final var channelsByType = detailData.getChannelsBySaveType(); + powerChannels.addAll(channelsByType.getOrDefault(HistoricTimedataSaveType.POWER, Collections.emptyList())); + energyChannels.addAll(channelsByType.getOrDefault(HistoricTimedataSaveType.ENERGY, Collections.emptyList())); + var powerData = this.timedata.queryHistoricData(null, request.getFromDate(), request.getToDate(), + powerChannels, new Resolution(15, ChronoUnit.MINUTES)); + + var energyData = this.timedata.queryHistoricEnergy(null, request.getFromDate(), request.getToDate(), + energyChannels); + if (powerData == null || energyData == null) { + return null; + } + try { + return new QueryHistoricTimeseriesExportXlsxResponse(request.getId(), null, request.getFromDate(), + request.getToDate(), powerData, energyData, language, detailData); + + } catch (IOException e) { + throw new OpenemsException("QueryHistoricTimeseriesExportXlxsRequest failed: " + e.getMessage()); + } + } + private final Timedata getTimedata() throws OpenemsException { final var currentTimedata = this.timedata; if (currentTimedata == null) { diff --git a/io.openems.edge.controller.api.modbus/bnd.bnd b/io.openems.edge.controller.api.modbus/bnd.bnd index 420b544a143..87fd5d5a4a8 100644 --- a/io.openems.edge.controller.api.modbus/bnd.bnd +++ b/io.openems.edge.controller.api.modbus/bnd.bnd @@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.common,\ io.openems.edge.controller.api,\ io.openems.edge.controller.api.common,\ + io.openems.edge.ess.api,\ io.openems.edge.timedata.api,\ io.openems.wrapper.fastexcel diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java index 52e5f67deae..435021672eb 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java @@ -529,6 +529,6 @@ public boolean equals(Object other) { * @return component_channelId as String */ public static String formatChannelName(WriteChannel channel) { - return channel.getComponent().alias() + "_" + channel.channelId().name(); + return channel.getComponent().id() + "_" + channel.channelId().name(); } } diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/jsonrpc/GetModbusProtocolExportXlsxResponse.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/jsonrpc/GetModbusProtocolExportXlsxResponse.java index 21f2447965c..b86d0a983bd 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/jsonrpc/GetModbusProtocolExportXlsxResponse.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/jsonrpc/GetModbusProtocolExportXlsxResponse.java @@ -19,6 +19,7 @@ import io.openems.edge.common.modbusslave.ModbusRecordString16; import io.openems.edge.common.modbusslave.ModbusRecordUint16; import io.openems.edge.common.modbusslave.ModbusRecordUint32; +import io.openems.edge.common.modbusslave.ModbusRecordUint64; import io.openems.edge.common.modbusslave.ModbusType; /** @@ -148,25 +149,14 @@ private static void addUndefinedSheet(Workbook wb) { var nextRow = 2; for (ModbusType modbusType : ModbusType.values()) { - byte[] value = {}; - switch (modbusType) { - case FLOAT32: - value = ModbusRecordFloat32.UNDEFINED_VALUE; - break; - case FLOAT64: - value = ModbusRecordFloat64.UNDEFINED_VALUE; - break; - case STRING16: - value = ModbusRecordString16.UNDEFINED_VALUE; - break; - case ENUM16: - case UINT16: - value = ModbusRecordUint16.UNDEFINED_VALUE; - break; - case UINT32: - value = ModbusRecordUint32.UNDEFINED_VALUE; - break; - } + byte[] value = switch (modbusType) { + case FLOAT32 -> ModbusRecordFloat32.UNDEFINED_VALUE; + case FLOAT64 -> ModbusRecordFloat64.UNDEFINED_VALUE; + case STRING16 -> ModbusRecordString16.UNDEFINED_VALUE; + case ENUM16, UINT16 -> ModbusRecordUint16.UNDEFINED_VALUE; + case UINT32 -> ModbusRecordUint32.UNDEFINED_VALUE; + case UINT64 -> ModbusRecordUint64.UNDEFINED_VALUE; + }; nextRow++; ws.value(nextRow, 0, modbusType.toString()); ws.value(nextRow, 1, byteArrayToString(value)); diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java index 74055d005b1..6f1a47ad49c 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java @@ -1,5 +1,8 @@ package io.openems.edge.controller.api.modbus.readwrite; +import static io.openems.edge.common.channel.ChannelId.channelIdCamelToUpper; +import static io.openems.edge.common.channel.ChannelId.channelIdUpperToCamel; + import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -27,20 +30,26 @@ import io.openems.common.channel.AccessMode; import io.openems.common.channel.PersistencePriority; +import io.openems.common.channel.Unit; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; -import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.ChannelId.ChannelIdImpl; import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.IntegerReadChannel; import io.openems.edge.common.channel.WriteChannel; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.jsonapi.ComponentJsonApi; import io.openems.edge.common.meta.Meta; +import io.openems.edge.common.modbusslave.ModbusSlave; +import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; +import io.openems.edge.common.modbusslave.ModbusSlaveTable; +import io.openems.edge.common.modbusslave.ModbusType; import io.openems.edge.controller.api.Controller; import io.openems.edge.controller.api.common.Status; import io.openems.edge.controller.api.common.WriteObject; import io.openems.edge.controller.api.modbus.AbstractModbusTcpApi; import io.openems.edge.controller.api.modbus.ModbusTcpApi; +import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateActiveTime; @@ -52,20 +61,23 @@ configurationPolicy = ConfigurationPolicy.REQUIRE // ) public class ControllerApiModbusTcpReadWriteImpl extends AbstractModbusTcpApi - implements ControllerApiModbusTcpReadWrite, ModbusTcpApi, Controller, OpenemsComponent, ComponentJsonApi, TimedataProvider { + implements ControllerApiModbusTcpReadWrite, ModbusTcpApi, Controller, OpenemsComponent, ComponentJsonApi, + TimedataProvider, ModbusSlave { private final Logger log = LoggerFactory.getLogger(ControllerApiModbusTcpReadWriteImpl.class); - + private final CalculateActiveTime calculateCumulatedActiveTime = new CalculateActiveTime(this, ControllerApiModbusTcpReadWrite.ChannelId.CUMULATED_ACTIVE_TIME); - + private final CalculateActiveTime calculateCumulatedInactiveTime = new CalculateActiveTime(this, ControllerApiModbusTcpReadWrite.ChannelId.CUMULATED_INACTIVE_TIME); - + private List writeChannels; - + + private List components = new ArrayList<>(); + private boolean isActive = false; - + @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) private volatile Timedata timedata = null; @@ -83,10 +95,12 @@ public class ControllerApiModbusTcpReadWriteImpl extends AbstractModbusTcpApi ) protected void addComponent(OpenemsComponent component) { super.addComponent(component); + this.components.add(component); } protected void removeComponent(OpenemsComponent component) { super.removeComponent(component); + this.components.remove(component); } public ControllerApiModbusTcpReadWriteImpl() { @@ -105,7 +119,6 @@ private void activate(ComponentContext context, Config config) throws ModbusExce new ConfigRecord(config.id(), config.alias(), config.enabled(), this.metaComponent, config.component_ids(), config.apiTimeout(), config.port(), config.maxConcurrentConnections())); this.applyConfig(config); - this.handleTimeDataChannels(); } @Modified @@ -114,7 +127,6 @@ private void modified(ComponentContext context, Config config) throws OpenemsNam new ConfigRecord(config.id(), config.alias(), config.enabled(), this.metaComponent, config.component_ids(), config.apiTimeout(), config.port(), config.maxConcurrentConnections())); this.applyConfig(config); - this.handleTimeDataChannels(); } private void applyConfig(Config config) { @@ -126,12 +138,12 @@ private void applyConfig(Config config) { protected void deactivate() { super.deactivate(); } - + @Override public void run() throws OpenemsNamedException { this.isActive = false; super.run(); - + this.calculateCumulatedActiveTime.update(this.isActive); this.calculateCumulatedInactiveTime.update(!this.isActive); } @@ -166,7 +178,17 @@ private void configUpdate(String targetProperty, String requiredValue) { this.logError(this.log, "ERROR: " + e.getMessage()); } } - + + protected static String getChannelNameUpper(String componentId, + io.openems.edge.common.channel.ChannelId channelId) { + return channelIdCamelToUpper(componentId) + "_" + channelId.name(); + } + + protected static String getChannelNameCamel(String componentId, + io.openems.edge.common.channel.ChannelId channelId) { + return channelIdUpperToCamel(getChannelNameUpper(componentId, channelId)); + } + @Override protected Consumer, WriteObject>> handleWrites() { return entry -> { @@ -174,15 +196,19 @@ protected Consumer, WriteObject>> handleWrites() { WriteChannel channel = entry.getKey(); var writeObject = entry.getValue(); - String channelName = formatChannelName(channel); - var currentChannel = new ChannelIdImpl(channelName, - Doc.of(channel.getType()).persistencePriority(PersistencePriority.HIGH)); - if (!channels().stream().anyMatch(p -> p.channelId().name().equals(currentChannel.name()))) { - addChannel(currentChannel).setNextValue(writeObject.value()); - } else { - channel(currentChannel).setNextValue(writeObject.value()); + var channelNameCamel = getChannelNameCamel(channel.getComponent().id(), channel.channelId()); + + @SuppressWarnings("deprecation") + var logChannel = this._channel(channelNameCamel); + if (logChannel == null) { + var channelNameUpper = getChannelNameUpper(channel.getComponent().id(), channel.channelId()); + var currentChannel = new ChannelIdImpl(channelNameUpper, + Doc.of(channel.getType()).persistencePriority(PersistencePriority.HIGH)); + addChannel(currentChannel); + logChannel = channel(currentChannel); } - this.configUpdate("writeChannels", channel(currentChannel).channelId().id()); + logChannel.setNextValue(writeObject.value()); + this.configUpdate("writeChannels", logChannel.channelId().id()); }; } @@ -205,23 +231,28 @@ protected Runnable handleTimeouts() { public Timedata getTimedata() { return this.timedata; } - - /** - * Checks, if timedata channels are already set. - * If not, they will be created and added to current channels. - */ - protected void handleTimeDataChannels() { - var activeTimeChannel = new ChannelIdImpl("CUMULATED_ACTIVE_TIME", // - Doc.of(OpenemsType.DOUBLE).persistencePriority(PersistencePriority.HIGH)); - var inactiveTimeChannel = new ChannelIdImpl("CUMULATED_INACTIVE_TIME", // - Doc.of(OpenemsType.DOUBLE).persistencePriority(PersistencePriority.HIGH)); - - List timeChannels = Arrays.asList(activeTimeChannel, inactiveTimeChannel); - timeChannels.forEach(channel -> { - if (channels().stream().noneMatch(ch -> ch.channelId().id().equals(channel.id()))) { - addChannel(channel); - } - }); + + protected Integer getChannelValue(String componentId, io.openems.edge.common.channel.ChannelId channelId) { + @SuppressWarnings("deprecation") + var channel = this._channel(getChannelNameCamel(componentId, channelId)); + if (channel == null) { + return null; + } + return ((IntegerReadChannel) channel).value().get(); + } + + @Override + public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { + return new ModbusSlaveTable(// + OpenemsComponent.getModbusSlaveNatureTable(AccessMode.READ_ONLY), + ModbusSlaveNatureTable.of(ControllerApiModbusTcpReadWriteImpl.class, AccessMode.READ_ONLY, 300) + .cycleValue(0, this.id() + "/ Ess0ActivePowerLimit", Unit.WATT, "", ModbusType.FLOAT32, + t -> this.getChannelValue("ess0", + ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS)) + .cycleValue(2, this.id() + "/Ess0ReactivePowerLimit", Unit.WATT, "", ModbusType.FLOAT32, + t -> this.getChannelValue("ess0", + ManagedSymmetricEss.ChannelId.SET_REACTIVE_POWER_EQUALS)) + .build()); } } diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImplTest.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImplTest.java index a39ef6c3555..6248a9b4e41 100644 --- a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImplTest.java +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImplTest.java @@ -1,28 +1,27 @@ package io.openems.edge.controller.api.modbus.readonly; +import static io.openems.edge.controller.api.modbus.AbstractModbusTcpApi.DEFAULT_PORT; + import org.junit.Test; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.controller.api.modbus.AbstractModbusTcpApi; import io.openems.edge.controller.test.ControllerTest; public class ControllerApiModbusTcpReadOnlyImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerApiModbusTcpReadOnlyImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setEnabled(false) // do not actually start server .setComponentIds() // .setMaxConcurrentConnections(5) // - .setPort(AbstractModbusTcpApi.DEFAULT_PORT) // + .setPort(DEFAULT_PORT) // .build()) // .next(new TestCase()) // - ; + .deactivate(); } } diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java index 58ea9ca90c5..f87124a5295 100644 --- a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java @@ -1,43 +1,48 @@ package io.openems.edge.controller.api.modbus.readwrite; +import static io.openems.edge.controller.api.modbus.AbstractModbusTcpApi.DEFAULT_PORT; +import static io.openems.edge.controller.api.modbus.readwrite.ControllerApiModbusTcpReadWriteImpl.getChannelNameCamel; +import static io.openems.edge.controller.api.modbus.readwrite.ControllerApiModbusTcpReadWriteImpl.getChannelNameUpper; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; + import org.junit.Test; + import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.common.test.DummyCycle; -import io.openems.edge.controller.api.modbus.AbstractModbusTcpApi; import io.openems.edge.controller.test.ControllerTest; public class ControllerApiModbusTcpReadWriteImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerApiModbusTcpReadWriteImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setEnabled(false) // do not actually start server .setComponentIds() // .setMaxConcurrentConnections(5) // - .setPort(AbstractModbusTcpApi.DEFAULT_PORT) // + .setPort(DEFAULT_PORT) // .setApiTimeout(60) // .build()) // .next(new TestCase()) // + .deactivate(); ; } - + @Test public void testTimedataChannels() throws Exception { var controller = new ControllerApiModbusTcpReadWriteImpl(); // boolean channelNotFound = controller.channels().stream().noneMatch(// ch -> ch.channelId().id().equals("CumulatedActiveTime") // - || ch.channelId().id().equals("CumulatedInactiveTime")); // - assertFalse(channelNotFound); + || ch.channelId().id().equals("CumulatedInactiveTime")); // + assertFalse(channelNotFound); } - + @Test public void testAddFalseComponents() throws Exception { var controller = new ControllerApiModbusTcpReadWriteImpl(); // @@ -45,4 +50,16 @@ public void testAddFalseComponents() throws Exception { controller.getComponentNoModbusApiFaultChannel().nextProcessImage(); // assertTrue(controller.getComponentNoModbusApiFault().get()); // } + + @Test + public void testGetChannelNameUpper() { + assertEquals("ESS0_SET_ACTIVE_POWER_EQUALS", getChannelNameUpper("ess0", SET_ACTIVE_POWER_EQUALS)); + assertEquals("ESS0_SET_ACTIVE_POWER_EQUALS", getChannelNameUpper("Ess0", SET_ACTIVE_POWER_EQUALS)); + } + + @Test + public void testGetChannelNameCamel() { + assertEquals("Ess0SetActivePowerEquals", getChannelNameCamel("ess0", SET_ACTIVE_POWER_EQUALS)); + assertEquals("Ess0SetActivePowerEquals", getChannelNameCamel("Ess0", SET_ACTIVE_POWER_EQUALS)); + } } diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java index 5c06b078554..dfbd3ea52b8 100644 --- a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java @@ -1,12 +1,7 @@ package io.openems.edge.controller.api.modbus.readwrite; -import java.nio.channels.Channels; - import io.openems.common.test.AbstractComponentConfig; -import io.openems.common.types.EdgeConfig.Component.Channel; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.common.channel.ChannelId; -import io.openems.edge.common.channel.ChannelId.ChannelIdImpl; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.controller.api.mqtt/bnd.bnd b/io.openems.edge.controller.api.mqtt/bnd.bnd index ae32a25bd42..880e35262f0 100644 --- a/io.openems.edge.controller.api.mqtt/bnd.bnd +++ b/io.openems.edge.controller.api.mqtt/bnd.bnd @@ -5,8 +5,8 @@ Bundle-Version: 1.0.0.${tstamp} -buildpath: \ ${buildpath},\ - bcpkix;version='1.70',\ - bcprov;version='1.70',\ + bcpkix;version='1.78.1',\ + bcprov;version='1.78.1',\ io.openems.common,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ diff --git a/io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/ControllerApiMqttImplTest.java b/io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/ControllerApiMqttImplTest.java index 10981834b04..89486aa7d0b 100644 --- a/io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/ControllerApiMqttImplTest.java +++ b/io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/ControllerApiMqttImplTest.java @@ -1,5 +1,6 @@ package io.openems.edge.controller.api.mqtt; +import static io.openems.common.channel.PersistencePriority.VERY_LOW; import static io.openems.edge.controller.api.mqtt.ControllerApiMqttImpl.createTopicPrefix; import static org.junit.Assert.assertEquals; @@ -8,7 +9,6 @@ import org.junit.Test; -import io.openems.common.channel.PersistencePriority; import io.openems.common.test.TimeLeapClock; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.ComponentTest; @@ -16,8 +16,6 @@ public class ControllerApiMqttImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800L) /* starts at 1. January 2020 00:00:00 */, @@ -26,13 +24,13 @@ public void test() throws Exception { .addReference("componentManager", new DummyComponentManager(clock)) // .addComponent(new DummySum()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setClientId("edge0") // .setTopicPrefix("") // .setUsername("guest") // .setPassword("guest") // .setUri("ws://localhost:1883") // - .setPersistencePriority(PersistencePriority.VERY_LOW) // + .setPersistencePriority(VERY_LOW) // .setDebugMode(true) // .setCertPem("") // .setPrivateKeyPem("") // diff --git a/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/DummyJsonRpcRestHandlerFactory.java b/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/DummyJsonRpcRestHandlerFactory.java index 2b99bdb06c3..927c148ca6c 100644 --- a/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/DummyJsonRpcRestHandlerFactory.java +++ b/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/DummyJsonRpcRestHandlerFactory.java @@ -1,21 +1,19 @@ package io.openems.edge.controller.api.rest; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; import com.google.common.base.Supplier; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; public class DummyJsonRpcRestHandlerFactory extends JsonRpcRestHandler.Factory { - public DummyJsonRpcRestHandlerFactory(Supplier factoryMethod) - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyJsonRpcRestHandlerFactory(Supplier factoryMethod) throws ReflectionException { super(); - ReflectionUtils.setAttribute(JsonRpcRestHandler.Factory.class, this, "cso", - new DummyJsonRpcRestHandlerCso(factoryMethod)); + setAttributeViaReflection(this, "cso", new DummyJsonRpcRestHandlerCso(factoryMethod)); } private static class DummyJsonRpcRestHandlerCso implements ComponentServiceObjects { diff --git a/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/readonly/ControllerApiRestReadOnlyImplTest.java b/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/readonly/ControllerApiRestReadOnlyImplTest.java index e7bc5a974ef..04c9d124dd6 100644 --- a/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/readonly/ControllerApiRestReadOnlyImplTest.java +++ b/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/readonly/ControllerApiRestReadOnlyImplTest.java @@ -12,8 +12,6 @@ public class ControllerApiRestReadOnlyImplTest { - private static final String CTRL_ID = "ctrlApiRest0"; - @Test public void test() throws OpenemsException, Exception { final var port = TestUtils.findRandomOpenPortOnAllLocalInterfaces(); @@ -23,11 +21,12 @@ public void test() throws OpenemsException, Exception { .addReference("userService", new DummyUserService()) // .addReference("restHandlerFactory", new DummyJsonRpcRestHandlerFactory(JsonRpcRestHandler::new)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrlApiRest0") // .setEnabled(false) // do not actually start server .setConnectionlimit(5) // .setDebugMode(false) // .setPort(port) // - .build()); + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/readwrite/ControllerApiRestReadWriteImplTest.java b/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/readwrite/ControllerApiRestReadWriteImplTest.java index 6baf712a911..0dd9165587a 100644 --- a/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/readwrite/ControllerApiRestReadWriteImplTest.java +++ b/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/readwrite/ControllerApiRestReadWriteImplTest.java @@ -1,9 +1,11 @@ package io.openems.edge.controller.api.rest.readwrite; +import static io.openems.common.utils.JsonUtils.getAsJsonObject; import static io.openems.edge.common.test.DummyUser.DUMMY_ADMIN; import static io.openems.edge.common.test.DummyUser.DUMMY_GUEST; import static io.openems.edge.common.test.DummyUser.DUMMY_INSTALLER; import static io.openems.edge.common.test.DummyUser.DUMMY_OWNER; +import static io.openems.edge.controller.api.rest.readwrite.ControllerApiRestReadWrite.ChannelId.API_WORKER_LOG; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -27,7 +29,6 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.common.jsonrpc.request.GetEdgeConfigRequest; -import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.channel.Doc; @@ -51,9 +52,6 @@ public class ControllerApiRestReadWriteImplTest { - private static final String CTRL_ID = "ctrlApiRest0"; - private static final String DUMMY_ID = "dummy0"; - @Test public void test() throws OpenemsException, Exception { final var port = TestUtils.findRandomOpenPortOnAllLocalInterfaces(); @@ -81,10 +79,10 @@ public void test() throws OpenemsException, Exception { .addReference("userService", new DummyUserService(// DUMMY_GUEST, DUMMY_OWNER, DUMMY_INSTALLER, DUMMY_ADMIN)) // .addReference("restHandlerFactory", factory) // - .addComponent(new DummyComponent(DUMMY_ID) // + .addComponent(new DummyComponent("dummy0") // .withReadChannel(1234)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrlApiRest0") // .setApiTimeout(60) // .setConnectionlimit(5) // .setDebugMode(false) // @@ -98,7 +96,7 @@ public void test() throws OpenemsException, Exception { var channelGet = sendGetRequest(port, DUMMY_GUEST.password, "/rest/channel/dummy0/ReadChannel"); assertEquals(JsonUtils.buildJsonObject() // .addProperty("address", "dummy0/ReadChannel") // - .addProperty("type", "INTEGER") // + .addProperty("type", "INTEGER") // s .addProperty("accessMode", "RO") // .addProperty("text", "This is a Read-Channel") // .addProperty("unit", "W") // @@ -113,8 +111,8 @@ public void test() throws OpenemsException, Exception { assertEquals(new JsonObject(), channelPost); test // .next(new TestCase() // - .output(new ChannelAddress("dummy0", "WriteChannel"), 4321) // - .output(new ChannelAddress(CTRL_ID, "ApiWorkerLog"), "dummy0/WriteChannel:4321")); + .output("dummy0", DummyComponent.ChannelId.WRITE_CHANNEL, 4321) // + .output(API_WORKER_LOG, "dummy0/WriteChannel:4321")); // POST fails as GUEST try { @@ -132,7 +130,7 @@ public void test() throws OpenemsException, Exception { // POST successful as OWNER var request = new GetEdgeConfigRequest().toJsonObject(); JsonrpcResponseSuccess.from(// - JsonUtils.getAsJsonObject(// + getAsJsonObject(// sendPostRequest(port, DUMMY_OWNER.password, "/jsonrpc", request))); // POST fails as GUEST diff --git a/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/WsData.java b/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/WsData.java index a92eca5bba8..d70d2299659 100644 --- a/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/WsData.java +++ b/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/WsData.java @@ -142,14 +142,15 @@ public Optional getUser() { } @Override - public String toString() { - String tokenString; - if (this.sessionToken != null) { - tokenString = this.sessionToken.toString(); - } else { - tokenString = "UNKNOWN"; - } - return "WebsocketApi.WsData [sessionToken=" + tokenString + ", user=" + this.user + "]"; + public String toLogString() { + return new StringBuilder("WebsocketApi.WsData [sessionToken=") // + .append(this.sessionToken != null // + ? this.sessionToken.toString() // + : "UNKNOWN") // + .append(", user=") // + .append(this.user) // + .append("]") // + .toString(); } /** diff --git a/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/ControllerApiWebsocketImplTest.java b/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/ControllerApiWebsocketImplTest.java index b37f1fb4929..f2321dcc2cf 100644 --- a/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/ControllerApiWebsocketImplTest.java +++ b/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/ControllerApiWebsocketImplTest.java @@ -1,5 +1,7 @@ package io.openems.edge.controller.api.websocket; +import static io.openems.edge.controller.api.websocket.ControllerApiWebsocket.DEFAULT_PORT; + import org.junit.Test; import io.openems.edge.common.test.AbstractComponentTest.TestCase; @@ -8,20 +10,18 @@ public class ControllerApiWebsocketImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerApiWebsocketImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addReference("onRequestFactory", new DummyOnRequestFactory()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setApiTimeout(60) // - .setPort(ControllerApiWebsocket.DEFAULT_PORT) // + .setPort(DEFAULT_PORT) // .build()) // .next(new TestCase()) // - ; + .deactivate(); } } diff --git a/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/DummyOnRequestFactory.java b/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/DummyOnRequestFactory.java index a953b39803c..852eb16321e 100644 --- a/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/DummyOnRequestFactory.java +++ b/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/DummyOnRequestFactory.java @@ -1,17 +1,17 @@ package io.openems.edge.controller.api.websocket; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; public class DummyOnRequestFactory extends OnRequest.Factory { - public DummyOnRequestFactory() throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyOnRequestFactory() throws ReflectionException { super(); - ReflectionUtils.setAttribute(OnRequest.Factory.class, this, "cso", new DummyOnRequestCso()); + setAttributeViaReflection(this, "cso", new DummyOnRequestCso()); } private static class DummyOnRequestCso implements ComponentServiceObjects { diff --git a/io.openems.edge.controller.asymmetric.balancingcosphi/test/io/openems/edge/controller/asymmetric/balancingcosphi/ControllerAsymmetricBalancingCosPhiImplTest.java b/io.openems.edge.controller.asymmetric.balancingcosphi/test/io/openems/edge/controller/asymmetric/balancingcosphi/ControllerAsymmetricBalancingCosPhiImplTest.java index 8f59d84837e..8f8fee699d0 100644 --- a/io.openems.edge.controller.asymmetric.balancingcosphi/test/io/openems/edge/controller/asymmetric/balancingcosphi/ControllerAsymmetricBalancingCosPhiImplTest.java +++ b/io.openems.edge.controller.asymmetric.balancingcosphi/test/io/openems/edge/controller/asymmetric/balancingcosphi/ControllerAsymmetricBalancingCosPhiImplTest.java @@ -9,24 +9,20 @@ public class ControllerAsymmetricBalancingCosPhiImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - private static final String METER_ID = "meter0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerAsymmetricBalancingCosPhiImpl()) // - .addComponent(new DummyManagedAsymmetricEss(ESS_ID)) // - .addComponent(new DummyElectricityMeter(METER_ID)) // + .addComponent(new DummyManagedAsymmetricEss("ess0")) // + .addComponent(new DummyElectricityMeter("meter0")) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .setId("ctrl0") // + .setEssId("ess0") // + .setMeterId("meter0") // .setCosPhi(0.9) // .setDirection(CosPhiDirection.CAPACITIVE) // - .build()); // - ; + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.asymmetric.fixreactivepower/test/io/openems/edge/controller/asymmetric/fixreactivepower/ControllerAsymmetricFixReactivePowerImplTest.java b/io.openems.edge.controller.asymmetric.fixreactivepower/test/io/openems/edge/controller/asymmetric/fixreactivepower/ControllerAsymmetricFixReactivePowerImplTest.java index 53a2d3a18c1..f0c26cb202d 100644 --- a/io.openems.edge.controller.asymmetric.fixreactivepower/test/io/openems/edge/controller/asymmetric/fixreactivepower/ControllerAsymmetricFixReactivePowerImplTest.java +++ b/io.openems.edge.controller.asymmetric.fixreactivepower/test/io/openems/edge/controller/asymmetric/fixreactivepower/ControllerAsymmetricFixReactivePowerImplTest.java @@ -9,23 +9,20 @@ public class ControllerAsymmetricFixReactivePowerImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerAsymmetricFixReactivePowerImpl()) // - .addComponent(new DummyManagedAsymmetricEss(ESS_ID)) // + .addComponent(new DummyManagedAsymmetricEss("ess0")) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setPowerL1(0) // .setPowerL2(0) // .setPowerL3(0) // .build()) // - .next(new TestCase()); // - ; + .next(new TestCase()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.asymmetric.peakshaving/test/io/openems/edge/controller/asymmetric/peakshaving/ControllerAsymmetricPeakShavingImplTest.java b/io.openems.edge.controller.asymmetric.peakshaving/test/io/openems/edge/controller/asymmetric/peakshaving/ControllerAsymmetricPeakShavingImplTest.java index e8090feb15a..7d59fb27424 100644 --- a/io.openems.edge.controller.asymmetric.peakshaving/test/io/openems/edge/controller/asymmetric/peakshaving/ControllerAsymmetricPeakShavingImplTest.java +++ b/io.openems.edge.controller.asymmetric.peakshaving/test/io/openems/edge/controller/asymmetric/peakshaving/ControllerAsymmetricPeakShavingImplTest.java @@ -1,8 +1,13 @@ package io.openems.edge.controller.asymmetric.peakshaving; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L1; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L2; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L3; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; @@ -12,128 +17,117 @@ public class ControllerAsymmetricPeakShavingImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String METER_ID = "meter0"; - private static final ChannelAddress GRID_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - private static final ChannelAddress GRID_ACTIVE_POWER_L1 = new ChannelAddress(METER_ID, "ActivePowerL1"); - private static final ChannelAddress GRID_ACTIVE_POWER_L2 = new ChannelAddress(METER_ID, "ActivePowerL2"); - private static final ChannelAddress GRID_ACTIVE_POWER_L3 = new ChannelAddress(METER_ID, "ActivePowerL3"); - - private static final String ESS_ID = "ess0"; - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - @Test public void symmetricMeterTest() throws Exception { new ControllerTest(new ControllerAsymmetricPeakShavingImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyElectricityMeter(METER_ID)) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID) // + .addComponent(new DummyElectricityMeter("meter0")) // + .addComponent(new DummyManagedSymmetricEss("ess0") // .setPower(new DummyPower(0.3, 0.3, 0.1))) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setMeterId(METER_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setMeterId("meter0") // + .setEssId("ess0") // .setPeakShavingPower(33333) // .setRechargePower(16666) // .build()) .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(GRID_ACTIVE_POWER, 120000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 6000)) // + .input("ess0", ACTIVE_POWER, 0) // + .input("meter0", ACTIVE_POWER, 120000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 6000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(GRID_ACTIVE_POWER, 120000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 12001)) // + .input("ess0", ACTIVE_POWER, 0) // + .input("meter0", ACTIVE_POWER, 120000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 12001)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 3793) // - .input(GRID_ACTIVE_POWER, 120000 - 3793) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 16484)) // + .input("ess0", ACTIVE_POWER, 3793) // + .input("meter0", ACTIVE_POWER, 120000 - 3793) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 16484)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 8981) // - .input(GRID_ACTIVE_POWER, 120000 - 8981) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19650)) // + .input("ess0", ACTIVE_POWER, 8981) // + .input("meter0", ACTIVE_POWER, 120000 - 8981) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19650)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 13723) // - .input(GRID_ACTIVE_POWER, 120000 - 13723) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 21578)) // + .input("ess0", ACTIVE_POWER, 13723) // + .input("meter0", ACTIVE_POWER, 120000 - 13723) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 21578)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 17469) // - .input(GRID_ACTIVE_POWER, 120000 - 17469) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 22437)) // + .input("ess0", ACTIVE_POWER, 17469) // + .input("meter0", ACTIVE_POWER, 120000 - 17469) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 22437)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 20066) // - .input(GRID_ACTIVE_POWER, 120000 - 20066) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 22533)) // + .input("ess0", ACTIVE_POWER, 20066) // + .input("meter0", ACTIVE_POWER, 120000 - 20066) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 22533)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 21564) // - .input(GRID_ACTIVE_POWER, 120000 - 21564) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 22174)) // + .input("ess0", ACTIVE_POWER, 21564) // + .input("meter0", ACTIVE_POWER, 120000 - 21564) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 22174)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 22175) // - .input(GRID_ACTIVE_POWER, 120000 - 22175) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 21610)) // + .input("ess0", ACTIVE_POWER, 22175) // + .input("meter0", ACTIVE_POWER, 120000 - 22175) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 21610)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 22173) // - .input(GRID_ACTIVE_POWER, 120000 - 22173) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 21020)) // + .input("ess0", ACTIVE_POWER, 22173) // + .input("meter0", ACTIVE_POWER, 120000 - 22173) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 21020)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 21816) // - .input(GRID_ACTIVE_POWER, 120000 - 21816) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 20511)) // + .input("ess0", ACTIVE_POWER, 21816) // + .input("meter0", ACTIVE_POWER, 120000 - 21816) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 20511)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 21311) // - .input(GRID_ACTIVE_POWER, 120000 - 21311) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 20133)) // + .input("ess0", ACTIVE_POWER, 21311) // + .input("meter0", ACTIVE_POWER, 120000 - 21311) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 20133)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 20803) // - .input(GRID_ACTIVE_POWER, 120000 - 20803) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19893)) // + .input("ess0", ACTIVE_POWER, 20803) // + .input("meter0", ACTIVE_POWER, 120000 - 20803) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19893)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 20377) // - .input(GRID_ACTIVE_POWER, 120000 - 20377) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19772)); // + .input("ess0", ACTIVE_POWER, 20377) // + .input("meter0", ACTIVE_POWER, 120000 - 20377) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19772)) // + .deactivate(); } @Test public void asymmetricMeterTest() throws Exception { new ControllerTest(new ControllerAsymmetricPeakShavingImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyElectricityMeter(METER_ID)) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID) // + .addComponent(new DummyElectricityMeter("meter0")) // + .addComponent(new DummyManagedSymmetricEss("ess0") // .setPower(new DummyPower(0.3, 0.3, 0.1))) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setMeterId(METER_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setMeterId("meter0") // + .setEssId("ess0") // .setPeakShavingPower(33333) // .setRechargePower(16666) // .build()) .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(GRID_ACTIVE_POWER_L1, 20000) // - .input(GRID_ACTIVE_POWER_L2, 40000) // - .input(GRID_ACTIVE_POWER_L3, 10000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 6000)) // - .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(GRID_ACTIVE_POWER_L1, 20000) // - .input(GRID_ACTIVE_POWER_L2, 40000) // - .input(GRID_ACTIVE_POWER_L3, 10000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 12001)) // - .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 3793) // - .input(GRID_ACTIVE_POWER_L1, 20000 - 3793 / 3) // - .input(GRID_ACTIVE_POWER_L2, 40000 - 3793 / 3) // - .input(GRID_ACTIVE_POWER_L3, 10000 - 3793 / 3) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 16484)) // - .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 8981) // - .input(GRID_ACTIVE_POWER_L1, 20000 - 8981 / 3) // - .input(GRID_ACTIVE_POWER_L2, 40000 - 8981 / 3) // - .input(GRID_ACTIVE_POWER_L3, 10000 - 8981 / 3) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19651)); // + .input("ess0", ACTIVE_POWER, 0) // + .input("meter0", ACTIVE_POWER_L1, 20000) // + .input("meter0", ACTIVE_POWER_L2, 40000) // + .input("meter0", ACTIVE_POWER_L3, 10000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 6000)) // + .next(new TestCase() // + .input("ess0", ACTIVE_POWER, 0) // + .input("meter0", ACTIVE_POWER_L1, 20000) // + .input("meter0", ACTIVE_POWER_L2, 40000) // + .input("meter0", ACTIVE_POWER_L3, 10000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 12001)) // + .next(new TestCase() // + .input("ess0", ACTIVE_POWER, 3793) // + .input("meter0", ACTIVE_POWER_L1, 20000 - 3793 / 3) // + .input("meter0", ACTIVE_POWER_L2, 40000 - 3793 / 3) // + .input("meter0", ACTIVE_POWER_L3, 10000 - 3793 / 3) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 16484)) // + .next(new TestCase() // + .input("ess0", ACTIVE_POWER, 8981) // + .input("meter0", ACTIVE_POWER_L1, 20000 - 8981 / 3) // + .input("meter0", ACTIVE_POWER_L2, 40000 - 8981 / 3) // + .input("meter0", ACTIVE_POWER_L3, 10000 - 8981 / 3) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19651)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.asymmetric.phaserectification/test/io/openems/edge/controller/asymmetric/phaserectification/ControllerAsymmetricPhaseRectificationImplTest.java b/io.openems.edge.controller.asymmetric.phaserectification/test/io/openems/edge/controller/asymmetric/phaserectification/ControllerAsymmetricPhaseRectificationImplTest.java index f18c8669b3a..882d083baed 100644 --- a/io.openems.edge.controller.asymmetric.phaserectification/test/io/openems/edge/controller/asymmetric/phaserectification/ControllerAsymmetricPhaseRectificationImplTest.java +++ b/io.openems.edge.controller.asymmetric.phaserectification/test/io/openems/edge/controller/asymmetric/phaserectification/ControllerAsymmetricPhaseRectificationImplTest.java @@ -9,22 +9,18 @@ public class ControllerAsymmetricPhaseRectificationImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String METER_ID = "meter0"; - private static final String ESS_ID = "ess0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerAsymmetricPhaseRectificationImpl()) // - .addComponent(new DummyManagedAsymmetricEss(ESS_ID)) // - .addComponent(new DummyElectricityMeter(METER_ID)) // + .addComponent(new DummyManagedAsymmetricEss("ess0")) // + .addComponent(new DummyElectricityMeter("meter0")) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // - .build()); // - ; + .setId("ctrl0") // + .setEssId("ess0") // + .setMeterId("meter0") // + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.channelthreshold/test/io/openems/edge/controller/channelthreshold/ControllerChannelThresholdImplTest.java b/io.openems.edge.controller.channelthreshold/test/io/openems/edge/controller/channelthreshold/ControllerChannelThresholdImplTest.java index b5418c94413..5c2501c8981 100644 --- a/io.openems.edge.controller.channelthreshold/test/io/openems/edge/controller/channelthreshold/ControllerChannelThresholdImplTest.java +++ b/io.openems.edge.controller.channelthreshold/test/io/openems/edge/controller/channelthreshold/ControllerChannelThresholdImplTest.java @@ -8,7 +8,6 @@ public class ControllerChannelThresholdImplTest { - private static final String CTRL_ID = "ctrl0"; private static final ChannelAddress IO0_INPUT = new ChannelAddress("io0", "Input0"); private static final ChannelAddress IO0_OUTPUT = new ChannelAddress("io0", "Output0"); @@ -17,12 +16,12 @@ public void test() throws Exception { new ControllerTest(new ControllerChannelThresholdImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setInputChannelAddress(IO0_INPUT.toString()) // - .setOutputChannelAddress(IO0_OUTPUT.toString()) // + .setId("ctrl0") // + .setInputChannelAddress(IO0_INPUT) // + .setOutputChannelAddress(IO0_OUTPUT) // .setLowThreshold(40) // .setHighThreshold(80) // - .build()); // + .build()) // + .deactivate(); } - } diff --git a/io.openems.edge.controller.channelthreshold/test/io/openems/edge/controller/channelthreshold/MyConfig.java b/io.openems.edge.controller.channelthreshold/test/io/openems/edge/controller/channelthreshold/MyConfig.java index ab9677f984e..7bb1199c7bc 100644 --- a/io.openems.edge.controller.channelthreshold/test/io/openems/edge/controller/channelthreshold/MyConfig.java +++ b/io.openems.edge.controller.channelthreshold/test/io/openems/edge/controller/channelthreshold/MyConfig.java @@ -1,6 +1,7 @@ package io.openems.edge.controller.channelthreshold; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.ChannelAddress; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { @@ -22,13 +23,13 @@ public Builder setId(String id) { return this; } - public Builder setInputChannelAddress(String inputChannelAddress) { - this.inputChannelAddress = inputChannelAddress; + public Builder setInputChannelAddress(ChannelAddress inputChannelAddress) { + this.inputChannelAddress = inputChannelAddress.toString(); return this; } - public Builder setOutputChannelAddress(String outputChannelAddress) { - this.outputChannelAddress = outputChannelAddress; + public Builder setOutputChannelAddress(ChannelAddress outputChannelAddress) { + this.outputChannelAddress = outputChannelAddress.toString(); return this; } diff --git a/io.openems.edge.controller.chp.soc/test/io/openems/edge/controller/chp/soc/ControllerChpSocImplTest.java b/io.openems.edge.controller.chp.soc/test/io/openems/edge/controller/chp/soc/ControllerChpSocImplTest.java index 8937f49f4ce..a469e21f2bd 100644 --- a/io.openems.edge.controller.chp.soc/test/io/openems/edge/controller/chp/soc/ControllerChpSocImplTest.java +++ b/io.openems.edge.controller.chp.soc/test/io/openems/edge/controller/chp/soc/ControllerChpSocImplTest.java @@ -1,8 +1,10 @@ package io.openems.edge.controller.chp.soc; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.SOC; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; @@ -11,56 +13,49 @@ public class ControllerChpSocImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String ESS_ID = "ess0"; - private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); - - private static final String IO_ID = "io0"; - private static final ChannelAddress IO_OUTPUT0 = new ChannelAddress(IO_ID, "InputOutput0"); - @Test public void test() throws Exception { new ControllerTest(new ControllerChpSocImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID)) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyManagedSymmetricEss("ess0")) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setInputChannelAddress(ESS_SOC.toString()) // - .setOutputChannelAddress(IO_OUTPUT0.toString()) // + .setId("ctrl0") // + .setInputChannelAddress("ess0/Soc") // + .setOutputChannelAddress("io0/InputOutput0") // .setLowThreshold(15) // .setHighThreshold(85) // .setMode(Mode.AUTOMATIC) // .setInvert(false) // .build()) .next(new TestCase() // - .input(ESS_SOC, 14) // - .output(IO_OUTPUT0, true)) // + .input("ess0", SOC, 14) // + .output("io0", INPUT_OUTPUT0, true)) // .next(new TestCase() // - .input(ESS_SOC, 50) // - .output(IO_OUTPUT0, null)) // + .input("ess0", SOC, 50) // + .output("io0", INPUT_OUTPUT0, null)) // .next(new TestCase() // - .input(ESS_SOC, 90) // - .output(IO_OUTPUT0, false)) // + .input("ess0", SOC, 90) // + .output("io0", INPUT_OUTPUT0, false)) // .next(new TestCase() // - .input(ESS_SOC, 50) // - .output(IO_OUTPUT0, null)) // + .input("ess0", SOC, 50) // + .output("io0", INPUT_OUTPUT0, null)) // .next(new TestCase() // - .input(ESS_SOC, 15) // - .output(IO_OUTPUT0, true)) // + .input("ess0", SOC, 15) // + .output("io0", INPUT_OUTPUT0, true)) // .next(new TestCase() // - .input(ESS_SOC, 85) // - .output(IO_OUTPUT0, false)) // + .input("ess0", SOC, 85) // + .output("io0", INPUT_OUTPUT0, false)) // .next(new TestCase() // - .input(ESS_SOC, 86) // - .output(IO_OUTPUT0, false)) // + .input("ess0", SOC, 86) // + .output("io0", INPUT_OUTPUT0, false)) // .next(new TestCase() // - .input(ESS_SOC, 14) // - .output(IO_OUTPUT0, true)) // + .input("ess0", SOC, 14) // + .output("io0", INPUT_OUTPUT0, true)) // .next(new TestCase() // - .input(ESS_SOC, 45) // - .output(IO_OUTPUT0, null)); + .input("ess0", SOC, 45) // + .output("io0", INPUT_OUTPUT0, null)) // + .deactivate(); } } \ No newline at end of file diff --git a/io.openems.edge.controller.debug.detailedlog/test/io/openems/edge/controller/debug/detailedlog/ControllerDebugDetailedLogImplTest.java b/io.openems.edge.controller.debug.detailedlog/test/io/openems/edge/controller/debug/detailedlog/ControllerDebugDetailedLogImplTest.java index ed138710c30..0ae17386d3d 100644 --- a/io.openems.edge.controller.debug.detailedlog/test/io/openems/edge/controller/debug/detailedlog/ControllerDebugDetailedLogImplTest.java +++ b/io.openems.edge.controller.debug.detailedlog/test/io/openems/edge/controller/debug/detailedlog/ControllerDebugDetailedLogImplTest.java @@ -7,16 +7,15 @@ public class ControllerDebugDetailedLogImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerDebugDetailedLogImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setComponentIds() // - .build()); // + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.debug.log/test/io/openems/edge/controller/debuglog/ControllerDebugLogImplTest.java b/io.openems.edge.controller.debug.log/test/io/openems/edge/controller/debuglog/ControllerDebugLogImplTest.java index 0f0d97848c6..beabd1ba311 100644 --- a/io.openems.edge.controller.debug.log/test/io/openems/edge/controller/debuglog/ControllerDebugLogImplTest.java +++ b/io.openems.edge.controller.debug.log/test/io/openems/edge/controller/debuglog/ControllerDebugLogImplTest.java @@ -1,5 +1,6 @@ package io.openems.edge.controller.debuglog; +import static io.openems.edge.common.sum.Sum.ChannelId.ESS_SOC; import static org.junit.Assert.assertEquals; import java.util.ArrayList; @@ -7,7 +8,6 @@ import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; @@ -16,20 +16,6 @@ public class ControllerDebugLogImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String DUMMY0_ID = "dummy0"; - private static final String DUMMY1_ID = "dummy1"; - private static final String DUMMY1_ALIAS = "This is Dummy1"; - private static final String DUMMY2_ID = "dummy2"; - private static final String DUMMY2_ALIAS = DUMMY2_ID; - private static final String DUMMY10_ID = "dummy10"; - - private static final String ANY_DUMMY = "dummy*"; - - private static final ChannelAddress SUM_ESS_SOC = new ChannelAddress("_sum", "EssSoc"); - private static final ChannelAddress SUM_FOO_BAR = new ChannelAddress("_sum", "FooBar"); - @Test public void test() throws Exception { List components = new ArrayList<>(); @@ -39,25 +25,26 @@ public String debugLog() { return "foo:bar"; } }); - components.add(new DummyController(DUMMY0_ID) { + components.add(new DummyController("dummy0") { @Override public String debugLog() { return "abc:xyz"; } }); - components.add(new DummyController(DUMMY1_ID, DUMMY1_ALIAS) { + components.add(new DummyController("dummy1", "This is Dummy1") { + @Override public String debugLog() { return "def:uvw"; } }); - components.add(new DummyController(DUMMY2_ID, DUMMY2_ALIAS) { + components.add(new DummyController("dummy2", "dummy2") { @Override public String debugLog() { return "ghi:rst"; } }); - components.add(new DummyController(DUMMY10_ID) { + components.add(new DummyController("dummy10") { @Override public String debugLog() { return "jkl:opq"; @@ -68,19 +55,14 @@ public String debugLog() { new ControllerTest(sut) // .addReference("components", components) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setShowAlias(true) // .setCondensedOutput(true) // - .setAdditionalChannels(new String[] { // - SUM_ESS_SOC.toString(), // - SUM_FOO_BAR.toString() // - }) // - .setIgnoreComponents(new String[] { // - DUMMY0_ID // - }) // + .setAdditionalChannels("_sum/EssSoc", "_sum/FooBar") // + .setIgnoreComponents("dummy0") // .build()) // .next(new TestCase() // - .input(SUM_ESS_SOC, 50)); + .input(ESS_SOC, 50)); assertEquals( "_sum[Core.Sum|foo:bar|EssSoc:50 %|FooBar:CHANNEL_IS_NOT_DEFINED] dummy1[This is Dummy1|def:uvw] dummy2[ghi:rst] dummy10[jkl:opq]", @@ -97,13 +79,14 @@ public String debugLog() { return "foo:bar"; } }); - components.add(new DummyController(DUMMY0_ID) { + components.add(new DummyController("dummy0") { @Override public String debugLog() { return "abc:xyz"; } }); - components.add(new DummyController(DUMMY1_ID) { + components.add(new DummyController("dummy1") { + @Override public String debugLog() { return "def:uvw"; @@ -116,17 +99,14 @@ public String debugLog() { .addComponent(components.get(0)) // .addComponent(components.get(1)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setCondensedOutput(true) // - .setAdditionalChannels(new String[] { // - SUM_ESS_SOC.toString() // - }) // - .setIgnoreComponents(new String[] { // - ANY_DUMMY // - }) // + .setAdditionalChannels("_sum/EssSoc") // + .setIgnoreComponents("dummy*") // .build()) // .next(new TestCase() // - .input(SUM_ESS_SOC, 50)); + .input(ESS_SOC, 50)) // + .deactivate(); assertEquals("_sum[foo:bar|EssSoc:50 %]", sut.getLogMessage()); diff --git a/io.openems.edge.controller.debug.log/test/io/openems/edge/controller/debuglog/MyConfig.java b/io.openems.edge.controller.debug.log/test/io/openems/edge/controller/debuglog/MyConfig.java index f23d1d89e1a..b8edc931126 100644 --- a/io.openems.edge.controller.debug.log/test/io/openems/edge/controller/debuglog/MyConfig.java +++ b/io.openems.edge.controller.debug.log/test/io/openems/edge/controller/debuglog/MyConfig.java @@ -25,12 +25,12 @@ public Builder setShowAlias(boolean showAlias) { return this; } - public Builder setAdditionalChannels(String[] additionalChannels) { + public Builder setAdditionalChannels(String... additionalChannels) { this.additionalChannels = additionalChannels; return this; } - public Builder setIgnoreComponents(String[] ignoreComponents) { + public Builder setIgnoreComponents(String... ignoreComponents) { this.ignoreComponents = ignoreComponents; return this; } diff --git a/io.openems.edge.controller.ess.acisland/test/io/openems/edge/controller/ess/acisland/ControllerEssAcIslandImplTest.java b/io.openems.edge.controller.ess.acisland/test/io/openems/edge/controller/ess/acisland/ControllerEssAcIslandImplTest.java index 1a25df66607..7208178a501 100644 --- a/io.openems.edge.controller.ess.acisland/test/io/openems/edge/controller/ess/acisland/ControllerEssAcIslandImplTest.java +++ b/io.openems.edge.controller.ess.acisland/test/io/openems/edge/controller/ess/acisland/ControllerEssAcIslandImplTest.java @@ -2,35 +2,26 @@ import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; public class ControllerEssAcIslandImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String ESS_ID = "ess0"; - - private static final String IO_ID = "io0"; - private static final ChannelAddress IO_OUTPUT0 = new ChannelAddress(IO_ID, "Output0"); - private static final ChannelAddress IO_OUTPUT1 = new ChannelAddress(IO_ID, "Output1"); - @Test public void test() throws Exception { new ControllerTest(new ControllerEssAcIslandImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setInvertOffGridOutput(false) // .setInvertOnGridOutput(false) // .setMaxSoc(90) // .setMinSoc(4) // - .setOffGridOutputChannelAddress(IO_OUTPUT0.toString()) // - .setOnGridOutputChannelAddress(IO_OUTPUT1.toString()) // + .setOffGridOutputChannelAddress("io0/Output0") // + .setOnGridOutputChannelAddress("io0/Output1") // .build()) // - ; + .deactivate(); } } 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 26f3eb8d98f..1c1cdea75dc 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 @@ -1,5 +1,9 @@ package io.openems.edge.controller.ess.activepowervoltagecharacteristic; +import static io.openems.common.utils.JsonUtils.buildJsonArray; +import static io.openems.common.utils.JsonUtils.buildJsonObject; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; + import java.time.Instant; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; @@ -8,7 +12,6 @@ import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; -import io.openems.common.utils.JsonUtils; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -18,11 +21,7 @@ public class CharacteristicImplTest { - private static final String CTRL_ID = "ctrlActivePowerVoltageCharacteristic0"; - private static final String ESS_ID = "ess1"; - private static final String METER_ID = "meter0"; - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "SetActivePowerEquals"); - private static final ChannelAddress METER_VOLTAGE = new ChannelAddress(METER_ID, "Voltage"); + private static final ChannelAddress METER_VOLTAGE = new ChannelAddress("meter0", "Voltage"); @Test public void test() throws Exception { @@ -30,36 +29,36 @@ public void test() throws Exception { new ControllerTest(new ControllerEssActivePowerVoltageCharacteristicImpl())// .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("meter", new DummyElectricityMeter(METER_ID)) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("meter", new DummyElectricityMeter("meter0")) // + .addReference("ess", new DummyManagedSymmetricEss("ess1")) // .activate(MyConfig.create()// - .setId(CTRL_ID)// - .setEssId(ESS_ID)// - .setMeterId(METER_ID)// + .setId("ctrlActivePowerVoltageCharacteristic0")// + .setEssId("ess1")// + .setMeterId("meter0")// .setNominalVoltage(240)// .setWaitForHysteresis(5)// - .setPowerVoltConfig(JsonUtils.buildJsonArray()// - .add(JsonUtils.buildJsonObject()// + .setPowerVoltConfig(buildJsonArray()// + .add(buildJsonObject()// .addProperty("voltageRatio", 0.95) // .addProperty("power", 4000) // .build()) // - .add(JsonUtils.buildJsonObject()// + .add(buildJsonObject()// .addProperty("voltageRatio", 0.98) // .addProperty("power", 1000) // .build()) // - .add(JsonUtils.buildJsonObject()// + .add(buildJsonObject()// .addProperty("voltageRatio", 0.98001) // .addProperty("power", 0) // .build()) // - .add(JsonUtils.buildJsonObject()// + .add(buildJsonObject()// .addProperty("voltageRatio", 1.02999) // .addProperty("power", 0) // .build()) // - .add(JsonUtils.buildJsonObject()// + .add(buildJsonObject()// .addProperty("voltageRatio", 1.03) // .addProperty("power", -1000) // .build()) // - .add(JsonUtils.buildJsonObject()// + .add(buildJsonObject()// .addProperty("voltageRatio", 1.05) // .addProperty("power", -4000) // .build() // @@ -67,52 +66,52 @@ public void test() throws Exception { ).build()) // .next(new TestCase("First Input") // .input(METER_VOLTAGE, 250_000) // [mV] - .output(ESS_ACTIVE_POWER, -2749)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, -2749)) // .next(new TestCase("Second Input, \"Power: -1500 \"") // .timeleap(clock, 5, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 248_000) // [mV] - .output(ESS_ACTIVE_POWER, -1499))// + .output("ess1", SET_ACTIVE_POWER_EQUALS, -1499))// .next(new TestCase() // .input(METER_VOLTAGE, 240_200) // [mV] - .output(ESS_ACTIVE_POWER, null)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, null)) // .next(new TestCase("Third Input, \"Power: 0 \"") // .timeleap(clock, 5, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 238_100) // [mV] - .output(ESS_ACTIVE_POWER, 0)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, 0)) // .next(new TestCase() // .input(METER_VOLTAGE, 240_000) // [mV] - .output(ESS_ACTIVE_POWER, null)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, null)) // .next(new TestCase() // .input(METER_VOLTAGE, 238_800)// [mV] - .output(ESS_ACTIVE_POWER, null)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, null)) // .next(new TestCase("Fourth Input, \"Power: 0 \"") // .timeleap(clock, 5, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 235_200) // [mV] - .output(ESS_ACTIVE_POWER, 998)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, 998)) // .next(new TestCase() // .timeleap(clock, 2, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 235_600) // [mV] - .output(ESS_ACTIVE_POWER, null)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, null)) // .next(new TestCase() // .timeleap(clock, 2, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 234_000) // [mV] - .output(ESS_ACTIVE_POWER, null)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, null)) // .next(new TestCase("Fifth Input, \"Power: 1625 \"") // .timeleap(clock, 1, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 233_700) // [mV] - .output(ESS_ACTIVE_POWER, 1625)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, 1625)) // .next(new TestCase("Fourth Input, \"Power: 0 \"") // .timeleap(clock, 5, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 225_000) // [mV] - .output(ESS_ACTIVE_POWER, 4000)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, 4000)) // .next(new TestCase("Smaller then Min Key, \"Power: 0 \"") // .timeleap(clock, 5, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 255_000) // [mV] - .output(ESS_ACTIVE_POWER, -4000)) // + .output("ess1", SET_ACTIVE_POWER_EQUALS, -4000)) // .next(new TestCase("Bigger than Max Key, \"Power: 0 \"") // .timeleap(clock, 5, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 270_000) // [mV] - .output(ESS_ACTIVE_POWER, -4000)) // - ; + .output("ess1", SET_ACTIVE_POWER_EQUALS, -4000)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.balancing/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java b/io.openems.edge.controller.ess.balancing/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java index e6a320f6733..4cab4cf6d5a 100644 --- a/io.openems.edge.controller.ess.balancing/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java +++ b/io.openems.edge.controller.ess.balancing/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java @@ -1,96 +1,90 @@ package io.openems.edge.controller.ess.balancing; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.ess.test.DummyPower; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.test.DummyElectricityMeter; public class BalancingImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String ESS_ID = "ess0"; - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - - private static final String METER_ID = "meter0"; - private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - @Test public void test() throws Exception { new ControllerTest(new ControllerEssBalancingImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .addReference("ess", new DummyManagedSymmetricEss("ess0") // .setPower(new DummyPower(0.3, 0.3, 0.1))) // - .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("meter", new DummyElectricityMeter("meter0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .setId("ctrl0") // + .setEssId("ess0") // + .setMeterId("meter0") // .setTargetGridSetpoint(0) // .build()) .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, 20000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 6000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 6000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, 20000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 12000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 12000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 3793) // - .input(METER_ACTIVE_POWER, 20000 - 3793) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 16483)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 3793) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 3793) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 16483)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 8981) // - .input(METER_ACTIVE_POWER, 20000 - 8981) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19649)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 8981) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 8981) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19649)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 13723) // - .input(METER_ACTIVE_POWER, 20000 - 13723) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 21577)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 13723) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 13723) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 21577)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 17469) // - .input(METER_ACTIVE_POWER, 20000 - 17469) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 22436)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 17469) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 17469) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 22436)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 20066) // - .input(METER_ACTIVE_POWER, 20000 - 20066) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 22531)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 20066) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 20066) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 22531)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 21564) // - .input(METER_ACTIVE_POWER, 20000 - 21564) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 22171)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 21564) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 21564) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 22171)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 22175) // - .input(METER_ACTIVE_POWER, 20000 - 22175) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 21608)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 22175) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 22175) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 21608)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 22173) // - .input(METER_ACTIVE_POWER, 20000 - 22173) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 21017)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 22173) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 22173) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 21017)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 21816) // - .input(METER_ACTIVE_POWER, 20000 - 21816) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 20508)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 21816) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 21816) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 20508)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 21311) // - .input(METER_ACTIVE_POWER, 20000 - 21311) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 20129)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 21311) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 21311) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 20129)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 20803) // - .input(METER_ACTIVE_POWER, 20000 - 20803) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19889)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 20803) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 20803) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19889)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 20377) // - .input(METER_ACTIVE_POWER, 20000 - 20377) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19767)); + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 20377) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 20000 - 20377) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19767)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.cycle/test/io/openems/edge/controller/ess/cycle/ControllerEssCycleImplTest.java b/io.openems.edge.controller.ess.cycle/test/io/openems/edge/controller/ess/cycle/ControllerEssCycleImplTest.java index 3018b8a775c..93c3db68b5e 100644 --- a/io.openems.edge.controller.ess.cycle/test/io/openems/edge/controller/ess/cycle/ControllerEssCycleImplTest.java +++ b/io.openems.edge.controller.ess.cycle/test/io/openems/edge/controller/ess/cycle/ControllerEssCycleImplTest.java @@ -1,5 +1,15 @@ package io.openems.edge.controller.ess.cycle; +import static io.openems.edge.controller.ess.cycle.ControllerEssCycle.ChannelId.COMPLETED_CYCLES; +import static io.openems.edge.controller.ess.cycle.ControllerEssCycle.ChannelId.STATE_MACHINE; +import static io.openems.edge.controller.ess.cycle.CycleOrder.START_WITH_DISCHARGE; +import static io.openems.edge.controller.ess.cycle.HybridEssMode.TARGET_AC; +import static io.openems.edge.controller.ess.cycle.Mode.MANUAL_ON; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.ALLOWED_CHARGE_POWER; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.ALLOWED_DISCHARGE_POWER; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.SOC; + import java.time.Instant; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; @@ -7,7 +17,6 @@ import org.junit.Test; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -18,74 +27,61 @@ public class ControllerEssCycleImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(CTRL_ID, - ControllerEssCycle.ChannelId.STATE_MACHINE.id()); - - private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); - private static final ChannelAddress MAX_CHARGE_POWER = new ChannelAddress(ESS_ID, "AllowedChargePower"); - private static final ChannelAddress MAX_DISCHARGE_POWER = new ChannelAddress(ESS_ID, "AllowedDischargePower"); - private static final ChannelAddress SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, "SetActivePowerEquals"); - - private static final ChannelAddress COMPLETED_CYCLES = new ChannelAddress(CTRL_ID, "CompletedCycles"); - @Test public void test() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2000-01-01T01:00:00.00Z"), ZoneOffset.UTC); final var power = new DummyPower(10_000); - final var ess = new DummyManagedSymmetricEss(ESS_ID) // + final var ess = new DummyManagedSymmetricEss("ess0") // .setPower(power); final var test = new ControllerTest(new ControllerEssCycleImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("ess", ess) // .activate(MyConfig.create()// - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setCycleOrder(CycleOrder.START_WITH_DISCHARGE) // + .setId("ctrl0") // + .setEssId("ess0") // + .setCycleOrder(START_WITH_DISCHARGE) // .setStandbyTime(10)// .setStartTime("2000-01-01 01:00")// .setMaxSoc(100)// .setMinSoc(0)// .setPower(10000)// - .setMode(Mode.MANUAL_ON)// - .setHybridEssMode(HybridEssMode.TARGET_AC)// + .setMode(MANUAL_ON)// + .setHybridEssMode(TARGET_AC)// .setTotalCycleNumber(3)// .setFinalSoc(50)// .build()); power.addEss(ess); test.next(new TestCase()// .input(STATE_MACHINE, State.UNDEFINED)// - .input(MAX_CHARGE_POWER, -10_000)// - .input(MAX_DISCHARGE_POWER, 10_000)// - .input(SET_ACTIVE_POWER_EQUALS, 10_000) // - .input(ESS_SOC, 50)) // + .input("ess0", ALLOWED_CHARGE_POWER, -10_000)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 10_000)// + .input("ess0", SET_ACTIVE_POWER_EQUALS, 10_000) // + .input("ess0", SOC, 50)) // .next(new TestCase("First Discharge") // .output(STATE_MACHINE, State.START_DISCHARGE))// .next(new TestCase()// - .input(MAX_CHARGE_POWER, -10_000)// - .input(MAX_DISCHARGE_POWER, 1000))// + .input("ess0", ALLOWED_CHARGE_POWER, -10_000)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 1000))// .next(new TestCase()// .timeleap(clock, 10, ChronoUnit.MINUTES))// .next(new TestCase()// .output(STATE_MACHINE, State.START_DISCHARGE))// .next(new TestCase()// - .input(MAX_CHARGE_POWER, -10_000)// - .input(MAX_DISCHARGE_POWER, 0))// + .input("ess0", ALLOWED_CHARGE_POWER, -10_000)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 0))// .next(new TestCase()// .timeleap(clock, 11, ChronoUnit.MINUTES))// .next(new TestCase("First Charge")// .output(STATE_MACHINE, State.CONTINUE_WITH_CHARGE))// .next(new TestCase()// - .input(MAX_CHARGE_POWER, -1000)// - .input(MAX_DISCHARGE_POWER, 10_000))// + .input("ess0", ALLOWED_CHARGE_POWER, -1000)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 10_000))// .next(new TestCase()// .output(STATE_MACHINE, State.CONTINUE_WITH_CHARGE))// .next(new TestCase()// - .input(MAX_CHARGE_POWER, 0)// - .input(MAX_DISCHARGE_POWER, 10_000))// + .input("ess0", ALLOWED_CHARGE_POWER, 0)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 10_000))// .next(new TestCase()// .timeleap(clock, 11, ChronoUnit.MINUTES))// .next(new TestCase() // @@ -94,17 +90,17 @@ public void test() throws Exception { .output(COMPLETED_CYCLES, 1)// .output(STATE_MACHINE, State.START_DISCHARGE))// .next(new TestCase("Second Discharge")// - .input(ESS_SOC, 0)// - .input(MAX_CHARGE_POWER, -10_000)// - .input(MAX_DISCHARGE_POWER, 0))// + .input("ess0", SOC, 0)// + .input("ess0", ALLOWED_CHARGE_POWER, -10_000)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 0))// .next(new TestCase()// .timeleap(clock, 11, ChronoUnit.MINUTES))// .next(new TestCase()// .output(STATE_MACHINE, State.CONTINUE_WITH_CHARGE))// .next(new TestCase("Second Charge")// - .input(ESS_SOC, 100)// - .input(MAX_CHARGE_POWER, 0)// - .input(MAX_DISCHARGE_POWER, 10_000))// + .input("ess0", SOC, 100)// + .input("ess0", ALLOWED_CHARGE_POWER, 0)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 10_000))// .next(new TestCase()// .timeleap(clock, 11, ChronoUnit.MINUTES))// .next(new TestCase("Second completed cycle") // @@ -113,17 +109,17 @@ public void test() throws Exception { .output(COMPLETED_CYCLES, 2)// .output(STATE_MACHINE, State.START_DISCHARGE))// .next(new TestCase("Third Discharge")// - .input(ESS_SOC, 0)// - .input(MAX_CHARGE_POWER, -10_000)// - .input(MAX_DISCHARGE_POWER, 0))// + .input("ess0", SOC, 0)// + .input("ess0", ALLOWED_CHARGE_POWER, -10_000)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 0))// .next(new TestCase()// .timeleap(clock, 11, ChronoUnit.MINUTES))// .next(new TestCase()// .output(STATE_MACHINE, State.CONTINUE_WITH_CHARGE))// .next(new TestCase("Third Charge")// - .input(ESS_SOC, 100)// - .input(MAX_CHARGE_POWER, 0)// - .input(MAX_DISCHARGE_POWER, 10_000))// + .input("ess0", SOC, 100)// + .input("ess0", ALLOWED_CHARGE_POWER, 0)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 10_000))// .next(new TestCase()// .timeleap(clock, 11, ChronoUnit.MINUTES))// .next(new TestCase("Cycle Number 3 Test")// @@ -132,11 +128,11 @@ public void test() throws Exception { .next(new TestCase()// .output(STATE_MACHINE, State.FINAL_SOC))// .next(new TestCase()// - .input(ESS_SOC, 50)// - .input(MAX_CHARGE_POWER, -10_000)// - .input(MAX_DISCHARGE_POWER, 10_000))// + .input("ess0", SOC, 50)// + .input("ess0", ALLOWED_CHARGE_POWER, -10_000)// + .input("ess0", ALLOWED_DISCHARGE_POWER, 10_000))// .next(new TestCase() // - .output(STATE_MACHINE, State.FINISHED))// - ; // + .output(STATE_MACHINE, State.FINISHED)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.delaycharge/test/io/openems/edge/controller/ess/delaycharge/ControllerEssDelayChargeImplTest.java b/io.openems.edge.controller.ess.delaycharge/test/io/openems/edge/controller/ess/delaycharge/ControllerEssDelayChargeImplTest.java index 4c495a15a52..7f8dc9e62c8 100644 --- a/io.openems.edge.controller.ess.delaycharge/test/io/openems/edge/controller/ess/delaycharge/ControllerEssDelayChargeImplTest.java +++ b/io.openems.edge.controller.ess.delaycharge/test/io/openems/edge/controller/ess/delaycharge/ControllerEssDelayChargeImplTest.java @@ -1,13 +1,14 @@ package io.openems.edge.controller.ess.delaycharge; +import static io.openems.edge.controller.ess.delaycharge.ControllerEssDelayCharge.ChannelId.CHARGE_POWER_LIMIT; +import static java.time.temporal.ChronoUnit.HOURS; + import java.time.Instant; import java.time.ZoneId; -import java.time.temporal.ChronoUnit; import org.junit.Test; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; @@ -15,11 +16,6 @@ public class ControllerEssDelayChargeImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final ChannelAddress CTRL_CHARGE_POWER_LIMIT = new ChannelAddress(CTRL_ID, "ChargePowerLimit"); - - private static final String ESS_ID = "ess0"; - @Test public void test() throws Exception { // Initialize mocked Clock @@ -27,32 +23,32 @@ public void test() throws Exception { Instant.ofEpochMilli(1546300800000L /* Tuesday, 1. January 2019 00:00:00 */), ZoneId.of("UTC")); new ControllerTest(new ControllerEssDelayChargeImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID) // + .addComponent(new DummyManagedSymmetricEss("ess0") // .withSoc(20) // .withCapacity(9000)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setTargetHour(15) // .build()) .next(new TestCase() // - .timeleap(clock, 6, ChronoUnit.HOURS) // = 6 am - .output(CTRL_CHARGE_POWER_LIMIT, 800)) + .timeleap(clock, 6, HOURS) // = 6 am + .output(CHARGE_POWER_LIMIT, 800)) .next(new TestCase() // - .timeleap(clock, 2, ChronoUnit.HOURS) // = 8 am - .output(CTRL_CHARGE_POWER_LIMIT, 1028)) + .timeleap(clock, 2, HOURS) // = 8 am + .output(CHARGE_POWER_LIMIT, 1028)) .next(new TestCase() // - .timeleap(clock, 2, ChronoUnit.HOURS) // = 10 am - .output(CTRL_CHARGE_POWER_LIMIT, 1440)) + .timeleap(clock, 2, HOURS) // = 10 am + .output(CHARGE_POWER_LIMIT, 1440)) .next(new TestCase() // - .timeleap(clock, 2, ChronoUnit.HOURS) // = 12 am - .output(CTRL_CHARGE_POWER_LIMIT, 2400)) + .timeleap(clock, 2, HOURS) // = 12 am + .output(CHARGE_POWER_LIMIT, 2400)) .next(new TestCase() // - .timeleap(clock, 2, ChronoUnit.HOURS) // = 14 am - .output(CTRL_CHARGE_POWER_LIMIT, 7200)) + .timeleap(clock, 2, HOURS) // = 14 am + .output(CHARGE_POWER_LIMIT, 7200)) .next(new TestCase() // - .timeleap(clock, 3, ChronoUnit.HOURS) // = 16 am - .output(CTRL_CHARGE_POWER_LIMIT, 0)); + .timeleap(clock, 3, HOURS) // = 16 am + .output(CHARGE_POWER_LIMIT, 0)) // + .deactivate(); } - } diff --git a/io.openems.edge.controller.ess.delayedselltogrid/test/io/openems/edge/controller/ess/delayedselltogrid/ControllerEssDelayedSellToGridImplTest.java b/io.openems.edge.controller.ess.delayedselltogrid/test/io/openems/edge/controller/ess/delayedselltogrid/ControllerEssDelayedSellToGridImplTest.java index 3d258054b4f..6843f1ca737 100644 --- a/io.openems.edge.controller.ess.delayedselltogrid/test/io/openems/edge/controller/ess/delayedselltogrid/ControllerEssDelayedSellToGridImplTest.java +++ b/io.openems.edge.controller.ess.delayedselltogrid/test/io/openems/edge/controller/ess/delayedselltogrid/ControllerEssDelayedSellToGridImplTest.java @@ -1,76 +1,70 @@ package io.openems.edge.controller.ess.delayedselltogrid; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.meter.test.DummyElectricityMeter; public class ControllerEssDelayedSellToGridImplTest { - private static final String CTRL_ID = "ctrlDelayedSellToGrid0"; - private static final String ESS_ID = "ess0"; - private static final String METER_ID = "meter0"; - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - @Test public void test() throws Exception { new ControllerTest(new ControllerEssDelayedSellToGridImpl())// .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("meter", new DummyElectricityMeter(METER_ID)) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("meter", new DummyElectricityMeter("meter0")) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create()// - .setId(CTRL_ID)// - .setEssId(ESS_ID)// - .setMeterId(METER_ID)// + .setId("ctrlDelayedSellToGrid0")// + .setEssId("ess0")// + .setMeterId("meter0")// .setSellToGridPowerLimit(12_500_000)// .setContinuousSellToGridPower(500_000).build())// .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, 0) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 500_000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 500_000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, -30_000)// - .output(ESS_SET_ACTIVE_POWER_EQUALS, 470_000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, -30_000)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 470_000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 500_000) // - .input(METER_ACTIVE_POWER, -500_000)// - .output(ESS_SET_ACTIVE_POWER_EQUALS, 500_000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 500_000) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, -500_000)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 500_000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 50_000) // - .input(METER_ACTIVE_POWER, -500_000)// - .output(ESS_SET_ACTIVE_POWER_EQUALS, 50_000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 50_000) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, -500_000)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 50_000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, -50_000) // - .input(METER_ACTIVE_POWER, -500_000)// - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -50_000) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, -500_000)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 150_000) // - .input(METER_ACTIVE_POWER, -500_000)// - .output(ESS_SET_ACTIVE_POWER_EQUALS, 150_000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 150_000) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, -500_000)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 150_000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, -1_500_000)// - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, -1_500_000)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, -100_000) // - .input(METER_ACTIVE_POWER, -15_000_000)// - .output(ESS_SET_ACTIVE_POWER_EQUALS, -2_600_000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -100_000) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, -15_000_000)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, -2_600_000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, -1_000_000) // - .input(METER_ACTIVE_POWER, -16_000_000)// - .output(ESS_SET_ACTIVE_POWER_EQUALS, -4_500_000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -1_000_000) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, -16_000_000)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, -4_500_000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, -16_000_000)// - .output(ESS_SET_ACTIVE_POWER_EQUALS, -3_500_000)) // - ; + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("meter0", SymmetricEss.ChannelId.ACTIVE_POWER, -16_000_000)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, -3_500_000)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.emergencycapacityreserve/bnd.bnd b/io.openems.edge.controller.ess.emergencycapacityreserve/bnd.bnd index 3f814d45c0c..0a8bfd62f05 100644 --- a/io.openems.edge.controller.ess.emergencycapacityreserve/bnd.bnd +++ b/io.openems.edge.controller.ess.emergencycapacityreserve/bnd.bnd @@ -8,6 +8,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.common,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ + io.openems.edge.energy.api,\ io.openems.edge.ess.api,\ io.openems.edge.ess.generic,\ diff --git a/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImpl.java b/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImpl.java index 35ba0a200aa..68e1ad4fc09 100644 --- a/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImpl.java +++ b/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImpl.java @@ -1,6 +1,10 @@ package io.openems.edge.controller.ess.emergencycapacityreserve; +import static io.openems.edge.energy.api.EnergyUtils.socToEnergy; +import static java.lang.Math.max; + import java.util.OptionalInt; +import java.util.function.Supplier; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -25,6 +29,8 @@ import io.openems.edge.controller.ess.emergencycapacityreserve.statemachine.Context; import io.openems.edge.controller.ess.emergencycapacityreserve.statemachine.StateMachine; import io.openems.edge.controller.ess.emergencycapacityreserve.statemachine.StateMachine.State; +import io.openems.edge.energy.api.EnergySchedulable; +import io.openems.edge.energy.api.EnergyScheduleHandler; import io.openems.edge.ess.api.ManagedSymmetricEss; @Designate(ocd = Config.class, factory = true) @@ -34,7 +40,7 @@ configurationPolicy = ConfigurationPolicy.REQUIRE // ) public class ControllerEssEmergencyCapacityReserveImpl extends AbstractOpenemsComponent - implements ControllerEssEmergencyCapacityReserve, Controller, OpenemsComponent { + implements ControllerEssEmergencyCapacityReserve, EnergySchedulable, Controller, OpenemsComponent { /** Minimum reserve SoC value in [%]. */ private static final int reservSocMinValue = 5; @@ -42,6 +48,7 @@ public class ControllerEssEmergencyCapacityReserveImpl extends AbstractOpenemsCo private static final int reservSocMaxValue = 100; private final Logger log = LoggerFactory.getLogger(ControllerEssEmergencyCapacityReserveImpl.class); + private final EnergyScheduleHandler energyScheduleHandler; private final StateMachine stateMachine = new StateMachine(State.NO_LIMIT); private final RampFilter rampFilter = new RampFilter(); @@ -65,6 +72,10 @@ public ControllerEssEmergencyCapacityReserveImpl() { Controller.ChannelId.values(), // ControllerEssEmergencyCapacityReserve.ChannelId.values() // ); + this.energyScheduleHandler = buildEnergyScheduleHandler(// + () -> this.config.isReserveSocEnabled() // + ? this.config.reserveSoc() // + : null); } @Activate @@ -77,6 +88,7 @@ private void activate(ComponentContext context, Config config) { protected void modified(ComponentContext context, String id, String alias, boolean enabled) { super.modified(context, id, alias, enabled); this.updateConfig(this.config); + this.energyScheduleHandler.triggerReschedule(); } @Override @@ -191,4 +203,31 @@ private OptionalInt getLastValidSoc(IntegerReadChannel channel) { .mapToInt(Value::get) // .findFirst(); } + + /** + * Builds the {@link EnergyScheduleHandler}. + * + *

+ * This is public so that it can be used by the EnergyScheduler integration + * test. + * + * @param minSoc supplier for the configured minSoc + * @return a {@link EnergyScheduleHandler} + */ + public static EnergyScheduleHandler buildEnergyScheduleHandler(Supplier minSoc) { + return EnergyScheduleHandler.of(// + simContext -> minSoc.get() == null // + ? null // + : socToEnergy(simContext.ess().totalEnergy(), minSoc.get()), // + (simContext, period, energyFlow, minEnergy) -> { + if (minEnergy != null) { + energyFlow.setEssMaxDischarge(max(0, simContext.getEssInitial() - minEnergy)); + } + }); + } + + @Override + public EnergyScheduleHandler getEnergyScheduleHandler() { + return this.energyScheduleHandler; + } } diff --git a/io.openems.edge.controller.ess.emergencycapacityreserve/test/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImplTest.java b/io.openems.edge.controller.ess.emergencycapacityreserve/test/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImplTest.java index 127acdc997d..4b8e05a2819 100644 --- a/io.openems.edge.controller.ess.emergencycapacityreserve/test/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImplTest.java +++ b/io.openems.edge.controller.ess.emergencycapacityreserve/test/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImplTest.java @@ -1,9 +1,19 @@ package io.openems.edge.controller.ess.emergencycapacityreserve; +import static io.openems.edge.common.sum.Sum.ChannelId.PRODUCTION_AC_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.PRODUCTION_DC_ACTUAL_POWER; +import static io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve.ChannelId.DEBUG_RAMP_POWER; +import static io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve.ChannelId.DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS; +import static io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve.ChannelId.DEBUG_TARGET_POWER; +import static io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve.ChannelId.RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE; +import static io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve.ChannelId.STATE_MACHINE; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_LESS_OR_EQUALS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.MAX_APPARENT_POWER; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.SOC; + import org.junit.Test; import io.openems.common.function.ThrowingRunnable; -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.DummyComponentManager; @@ -14,100 +24,82 @@ public class ControllerEssEmergencyCapacityReserveImplTest { - private static final String CTRL_ID = "ctrlEmergencyCapacityReserve0"; - private static final String ESS_ID = "ess0"; - private static final String SUM_ID = "_sum"; - - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(CTRL_ID, "StateMachine"); - private static final ChannelAddress DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS = new ChannelAddress(CTRL_ID, - "DebugSetActivePowerLessOrEquals"); - private static final ChannelAddress DEBUG_TARGET_POWER = new ChannelAddress(CTRL_ID, "DebugTargetPower"); - private static final ChannelAddress DEBUG_RAMP_POWER = new ChannelAddress(CTRL_ID, "DebugRampPower"); - - private static final ChannelAddress RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE = new ChannelAddress(CTRL_ID, - "RangeOfReserveSocOutsideAllowedValue"); - - private static final ChannelAddress ESS_MAX_APPARENT_POWER = new ChannelAddress(ESS_ID, "MaxApparentPower"); - private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); - private static final ChannelAddress SET_ACTIVE_POWER_LESS_OR_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerLessOrEquals"); - - private static final ChannelAddress PRODUCTION_DC_ACTUAL_POWER = new ChannelAddress(SUM_ID, - "ProductionDcActualPower"); - private static final ChannelAddress PRODUCTION_AC_ACTIVE_POWER = new ChannelAddress(SUM_ID, - "ProductionAcActivePower"); - @Test public void testReserveSocRange() throws Exception { new ControllerTest(new ControllerEssEmergencyCapacityReserveImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, false)); + .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, false)) // + .deactivate(); new ControllerTest(new ControllerEssEmergencyCapacityReserveImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(5) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, false)); + .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, false)) // + .deactivate(); new ControllerTest(new ControllerEssEmergencyCapacityReserveImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(4) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, true)); + .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, true)) // + .deactivate(); new ControllerTest(new ControllerEssEmergencyCapacityReserveImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(100) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, false)); + .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, false)) // + .deactivate(); new ControllerTest(new ControllerEssEmergencyCapacityReserveImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(101) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, true)); + .output(RANGE_OF_RESERVE_SOC_OUTSIDE_ALLOWED_VALUE, true)) // + .deactivate(); } @Test @@ -116,19 +108,19 @@ public void testReachTargetPower() throws Exception { .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // .output(STATE_MACHINE, State.NO_LIMIT)) // .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .output(STATE_MACHINE, State.NO_LIMIT)); // var maxApparentPower = 10000; @@ -141,14 +133,16 @@ public void testReachTargetPower() throws Exception { result -= rampPower; } - controllerTest.next(new TestCase().input(ESS_SOC, 21) // + controllerTest.next(new TestCase().input("ess0", SOC, 21) // .input(PRODUCTION_DC_ACTUAL_POWER, 0) // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, result) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, result) // .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, result) // .output(DEBUG_TARGET_POWER, targetPower.floatValue()) // ); } + + controllerTest.deactivate(); } @Test @@ -157,41 +151,42 @@ public void testAllStates() throws Exception { .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // .output(STATE_MACHINE, State.NO_LIMIT)) // .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .output(STATE_MACHINE, State.NO_LIMIT)) // .next(new TestCase() // - .input(ESS_SOC, 20) // + .input("ess0", SOC, 20) // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC)) // .next(new TestCase() // - .input(ESS_SOC, 19) // + .input("ess0", SOC, 19) // .output(STATE_MACHINE, State.AT_RESERVE_SOC)) // .next(new TestCase() // - .input(ESS_SOC, 16) // + .input("ess0", SOC, 16) // .output(STATE_MACHINE, State.BELOW_RESERVE_SOC)) // .next(new TestCase() // - .input(ESS_SOC, 20) // + .input("ess0", SOC, 20) // .output(STATE_MACHINE, State.FORCE_CHARGE)) // .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .output(STATE_MACHINE, State.AT_RESERVE_SOC)) // .next(new TestCase() // - .input(ESS_SOC, 22) // + .input("ess0", SOC, 22) // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC)) // .next(new TestCase() // - .input(ESS_SOC, 22) // - .output(STATE_MACHINE, State.NO_LIMIT)); + .input("ess0", SOC, 22) // + .output(STATE_MACHINE, State.NO_LIMIT)) // + .deactivate(); } @Test @@ -200,23 +195,24 @@ public void testIncreaseRampByNoLimitState() throws Exception { .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .input(ESS_SOC, 80)) // + .input("ess0", SOC, 80)) // .next(new TestCase() // - .input(ESS_SOC, 80) // - .input(ESS_MAX_APPARENT_POWER, 10000) // + .input("ess0", SOC, 80) // + .input("ess0", MAX_APPARENT_POWER, 10000) // .output(STATE_MACHINE, State.NO_LIMIT) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, null) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null) // .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, null) // .output(DEBUG_TARGET_POWER, 10000f) // - .output(DEBUG_RAMP_POWER, 100f)); + .output(DEBUG_RAMP_POWER, 100f)) // + .deactivate(); } @Test @@ -225,65 +221,66 @@ public void testDecreaseRampByAboveReserveSocState() throws Exception { .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // .output(STATE_MACHINE, State.NO_LIMIT)) // .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .output(STATE_MACHINE, State.NO_LIMIT)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, null) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null) // .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, null)) // // to reach 50% of maxApparentPower .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .input(PRODUCTION_DC_ACTUAL_POWER, 0) // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)) // .next(new TestCase() // - .input(ESS_SOC, 21) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// + .input("ess0", SOC, 21) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)) // .next(new TestCase() // - .input(ESS_SOC, 21) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)// + .input("ess0", SOC, 21) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)) // // to reach is DC-PV .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .input(PRODUCTION_DC_ACTUAL_POWER, 6000) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)) .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .input(PRODUCTION_DC_ACTUAL_POWER, 10000) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)) .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .input(PRODUCTION_DC_ACTUAL_POWER, 10000) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)) .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .input(PRODUCTION_DC_ACTUAL_POWER, 6000) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)) .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .input(PRODUCTION_DC_ACTUAL_POWER, 6000) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)// - .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)); + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)// + .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)) // + .deactivate(); } @Test @@ -292,41 +289,42 @@ public void testDecreaseRampByAtReserveSocState() throws Exception { .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // .output(STATE_MACHINE, State.NO_LIMIT)) // .next(new TestCase() // - .input(ESS_SOC, 20) // + .input("ess0", SOC, 20) // .output(STATE_MACHINE, State.NO_LIMIT)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, null)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, null)) // .next(new TestCase() // - .input(ESS_SOC, 20) // + .input("ess0", SOC, 20) // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)) // .next(new TestCase() // - .input(ESS_SOC, 20) // + .input("ess0", SOC, 20) // .input(PRODUCTION_DC_ACTUAL_POWER, 0) // .output(STATE_MACHINE, State.AT_RESERVE_SOC)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)) // .next(new TestCase() // - .input(ESS_SOC, 20) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)// + .input("ess0", SOC, 20) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)) // .next(new TestCase() // - .input(ESS_SOC, 20) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)// - .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)); + .input("ess0", SOC, 20) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)// + .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9600)) // + .deactivate(); } @Test @@ -335,45 +333,46 @@ public void testDecreaseRampByUnderReserveSocState() throws Exception { .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // .output(STATE_MACHINE, State.NO_LIMIT)) // .next(new TestCase() // - .input(ESS_SOC, 19) // + .input("ess0", SOC, 19) // .output(STATE_MACHINE, State.NO_LIMIT)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, null)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, null)) // .next(new TestCase() // - .input(ESS_SOC, 19) // + .input("ess0", SOC, 19) // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)) // .next(new TestCase() // - .input(ESS_SOC, 19) // + .input("ess0", SOC, 19) // .output(STATE_MACHINE, State.AT_RESERVE_SOC)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)) // .next(new TestCase() // - .input(ESS_SOC, 19) // + .input("ess0", SOC, 19) // .output(STATE_MACHINE, State.BELOW_RESERVE_SOC)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9300)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9300)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9300)) // .next(new TestCase() // - .input(ESS_SOC, 19) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 8800)// + .input("ess0", SOC, 19) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 8800)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 8800)) // .next(new TestCase() // - .input(ESS_SOC, 19) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 8300)// - .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 8300)); + .input("ess0", SOC, 19) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 8300)// + .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 8300)) // + .deactivate(); } @Test @@ -382,62 +381,63 @@ public void testDecreaseRampByForceStartChargeState() throws Exception { .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // .output(STATE_MACHINE, State.NO_LIMIT)) // .next(new TestCase() // - .input(ESS_SOC, 16) // + .input("ess0", SOC, 16) // .output(STATE_MACHINE, State.NO_LIMIT)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, null)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, null)) // .next(new TestCase() // - .input(ESS_SOC, 16) // + .input("ess0", SOC, 16) // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9900)) // .next(new TestCase() // - .input(ESS_SOC, 16) // + .input("ess0", SOC, 16) // .output(STATE_MACHINE, State.AT_RESERVE_SOC)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9800)) // .next(new TestCase() // - .input(ESS_SOC, 16) // + .input("ess0", SOC, 16) // .output(STATE_MACHINE, State.BELOW_RESERVE_SOC)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9300)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9300)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9300)) // .next(new TestCase() // - .input(ESS_SOC, 16) // + .input("ess0", SOC, 16) // .input(PRODUCTION_AC_ACTIVE_POWER, 100) // .output(STATE_MACHINE, State.FORCE_CHARGE)// - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9200)// + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9200)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9200)) // .next(new TestCase() // - .input(ESS_SOC, 19) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9100)// + .input("ess0", SOC, 19) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9100)// .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9100)) // .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .output(STATE_MACHINE, State.FORCE_CHARGE) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 9000) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9000) // .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9000)) // .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .output(STATE_MACHINE, State.AT_RESERVE_SOC) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 8900) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 8900) // .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 8900)) // .next(new TestCase() // - .input(ESS_SOC, 21) // + .input("ess0", SOC, 21) // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, 8800)// - .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 8800)); + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 8800)// + .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 8800)) // + .deactivate(); } @Test @@ -451,11 +451,11 @@ public void testUndefinedSoc() throws Exception { .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .addReference("ess", new DummyManagedSymmetricEss("ess0") // .withMaxApparentPower(10000)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) // @@ -464,16 +464,17 @@ public void testUndefinedSoc() throws Exception { .output(STATE_MACHINE, State.NO_LIMIT)) // .next(new TestCase() // .onAfterProcessImage(sleep) // - .input(ESS_SOC, 16)) // + .input("ess0", SOC, 16)) // .next(new TestCase() // .onAfterProcessImage(sleep) // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC))// .next(new TestCase() // .onAfterProcessImage(sleep) // - .input(ESS_SOC, null)) // + .input("ess0", SOC, null)) // .next(new TestCase() // .onAfterProcessImage(sleep) // - .output(STATE_MACHINE, State.BELOW_RESERVE_SOC)); + .output(STATE_MACHINE, State.BELOW_RESERVE_SOC)) // + .deactivate(); } @Test @@ -482,18 +483,18 @@ public void testIncreaseRampToMaxApparentPower() throws Exception { .addReference("componentManager", new DummyComponentManager()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .addReference("ess", new DummyManagedSymmetricEss("ess0") // .withMaxApparentPower(10000)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setReserveSoc(20) // .setReserveSocEnabled(true) // .build()) // .next(new TestCase() // .output(STATE_MACHINE, State.NO_LIMIT)) // .next(new TestCase() // - .input(ESS_SOC, 21)) // + .input("ess0", SOC, 21)) // .next(new TestCase() // .output(STATE_MACHINE, State.ABOVE_RESERVE_SOC) // .output(DEBUG_TARGET_POWER, 5000f) // @@ -510,7 +511,7 @@ public void testIncreaseRampToMaxApparentPower() throws Exception { .output(DEBUG_RAMP_POWER, 100f) // .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9700)) // .next(new TestCase() // - .input(ESS_SOC, 22)) // + .input("ess0", SOC, 22)) // .next(new TestCase() // .output(STATE_MACHINE, State.NO_LIMIT) // .output(DEBUG_TARGET_POWER, 10000f) // @@ -531,13 +532,14 @@ public void testIncreaseRampToMaxApparentPower() throws Exception { .output(DEBUG_TARGET_POWER, 10000f) // .output(DEBUG_RAMP_POWER, 100f) // .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, null) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, null)) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null)) // .next(new TestCase() // .output(STATE_MACHINE, State.NO_LIMIT) // .output(DEBUG_TARGET_POWER, 10000f) // .output(DEBUG_RAMP_POWER, 100f) // .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, null) // - .output(SET_ACTIVE_POWER_LESS_OR_EQUALS, null)); + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/.classpath b/io.openems.edge.controller.ess.fastfrequencyreserve/.classpath new file mode 100644 index 00000000000..bbfbdbe40e7 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/.gitignore b/io.openems.edge.controller.ess.fastfrequencyreserve/.gitignore new file mode 100644 index 00000000000..c2b941a96de --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/.gitignore @@ -0,0 +1,2 @@ +/bin_test/ +/generated/ diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/.project b/io.openems.edge.controller.ess.fastfrequencyreserve/.project new file mode 100644 index 00000000000..0140daa493d --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.controller.ess.fastfrequencyreserve + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.controller.ess.fastfrequencyreserve/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..896a9a53a53 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/bnd.bnd b/io.openems.edge.controller.ess.fastfrequencyreserve/bnd.bnd new file mode 100644 index 00000000000..bee07090a70 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/bnd.bnd @@ -0,0 +1,16 @@ +Bundle-Name: OpenEMS Edge Controller Fast Frequency Reserve +Bundle-Vendor: FENECON GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 1.0.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + Java-WebSocket,\ + io.openems.common,\ + io.openems.edge.common,\ + io.openems.edge.controller.api,\ + io.openems.edge.ess.api,\ + io.openems.edge.meter.api,\ + +-testpath: \ + ${testpath} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/doc/statemachine.md b/io.openems.edge.controller.ess.fastfrequencyreserve/doc/statemachine.md new file mode 100644 index 00000000000..23c71c2221a --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/doc/statemachine.md @@ -0,0 +1,18 @@ +# State-Machine + +```mermaid +graph TD +start --> Undefined +Undefined --> |condition: Inside set time, task : Charge to maintain soc| PreActivateState +PreActivateState --> |condition: Outside set time, task : do nothing| Undefined +PreActivateState --> |condition: grid freq > freqlimit, task : setpower 0Watt| ActivationTime +ActivationTime --> |condition: 1.7 sec, task : discharge setActivepower| SupportDuration +SupportDuration --> |condition: 30 sec, task : discharge setActivepower| DeactivationTime +DeactivationTime -->|condition: 1.7 sec, task : setpower 0Watt| BufferedTime +BufferedTime --> |condition: 10 sec, task : setpower for 0Watt| BufferedSupportTime +BufferedSupportTime --> |condition: 15 min, Charge to maintain soc| RecoveryTime +RecoveryTime --> PreActivateState +RecoveryTime -->|condition: Outside set time, task : do nothing| Undefined +``` + +View using Mermaid, e.g. https://mermaid-js.github.io/mermaid-live-editor \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/readme.adoc b/io.openems.edge.controller.ess.fastfrequencyreserve/readme.adoc new file mode 100644 index 00000000000..028257dcea9 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/readme.adoc @@ -0,0 +1,333 @@ += ESS Fast Frequency Reserve + +== 1.1 Introduction + +In electricity networks, the Fast Frequency Reserve (FFR) controller is providing power available to the system operator within a short interval to meet demand in case of a frequency drop, i.e. in case a generator goes down or there is another disruption to the supply. More details on link:https://en.wikipedia.org/wiki/Operating_reserve[Wikipedia]. + +This controller helps the Energy Storage System (ESS) to provide power, essentially battery discharge, when the measured "Grid frequency" is lower than that of a defined "Frequency limit". + +== 1.2 Controller Parameters + +- **mode**: mode of the controller, On or Off? +- **id**: the id for the controller +- **alias**: Alias for the controller +- **enabled**: enabled or not? +- **meterId**: the id of the meter +- **essId**: the id of the Ess +- **batteryInverterId**: the id of the battery inverter +- **preActivationTime**: A time before the activation time for charging the system(min). +- **schedule**: scheduling of the controller, via JSON see below for the example + +=== 1.2.1 The Example Schedule-JSON + +[source,json] +---- +[ + { + "startTimestamp": 1684792800, + "duration": 86400, + "dischargePowerSetPoint": 92000, + "frequencyLimit": 50000, + "activationRunTime": "LONG_ACTIVATION_RUN", + "supportDuration": "LONG_SUPPORT_DURATION" + }, + { + "startTimestamp": 1684879200, + "duration": 86400, + "dischargePowerSetPoint": 6000, + "frequencyLimit": 50000, + "activationRunTime": "LONG_ACTIVATION_RUN", + "supportDuration": "LONG_SUPPORT_DURATION" + } +] +---- + +=== 1.2.2 JSON Element details + +- `StartTimeStamp`: When the controller should be activated. +- `Duration`: How long is the controller to be activated? +- `frequency limit`: The controller continuously monitors and checks whether a Frequency limit or threshold is less than the +measured grid frequency. +- `DischargePower`: The Ess discharges from the batteries when generating capacity. +- `activationRunTime`: The time in milliseconds required for the reserve to fully activate. Short(700 ms) or Medium(1000 ms) or Long(1300 ms) activation Time. +- `supportDuration`: The time in milliseconds for which the reserve should continue providing support after the frequency has stabilized. Short(5 seconds) or Long(30 seconds) support duration. + +=== 1.2.3 Explanation of the Schedule +The Schedule JSON activates FFR for a full day (86400 seconds or 24 hours) with the following parameters: + +1. Schedule for 23rd May 2023 00:00:00 to 24th May: + - *Threshold frequency:* 49700 mHz + - *Discharge power:* 92000 W + - *Long activation time:* 1.3 seconds + - *Support duration:* 30 seconds + +2. Following Schedule for 24th May 2023 00:00:00 to 25th May: + - *Threshold frequency:* 49700 mHz + - *Discharge power:* 52000 W + - *Long activation time:* 1.3 seconds + - *Support duration:* 30 seconds + + +== 2.1 REST API for updating Fast Frequency Reserve controllers schedule locally + +note : The controller/ App should be activated to update the schedule, which can be done using online monitoring or apache felix. + +== 2.1.1 Overview + +This REST API allows you to update FFR schedule for the specified edge device. The API endpoint takes a JSON payload that specifies the schedule, including the start time, duration, discharge power set point, frequency limit, activation runtime, and support duration. + +== 2.1.2 Endpoint + +- *URL*: http://:8084/jsonrpc +- *Method*: POST +- *Content-Type*: application/json +- *Authorization*: Basic Authentication, username: x, password: owner + +== 2.1.3 Body + +The request body must be a JSON object with the following structure: + +[source,json] +---- +{ + "method": "componentJsonApi", + "params": { + "componentId": "ctrlFastFreqReserve0", + "payload": { + "method": "setActivateFastFreqReserve", + "params": { + "id": "edge0", + "schedule": [ + { + "startTimestamp": 1701871562, + "duration": 999, + "dischargePowerSetPoint": 6000, + "frequencyLimit": 502000, + "activationRunTime": "LONG_ACTIVATION_RUN", + "supportDuration": "LONG_SUPPORT_DURATION" + } + ] + } + } + } +} +---- + +== 2.1.4 Request Parameters + +The request body for this REST API call is a JSON object with the following parameters: + +- *method*: The specific method to call within the component. In this case, it is `componentJsonApi`. +- *params*: The parameters associated with the method call. +- *componentId*: The unique identifier of the component that is receiving the request. +- *payload*: The specific data being sent to the component. See below + +=== 2.1.5 Payload Parameters + +Within the payload parameter, there is another JSON object that specifies the details of the activation request: + +- *method*: The method to call within the component to handle the activation request. In this case, it is `setActivateFastFreqReserve`. +- *params*: The parameters associated with the `setActivateFastFreqReserve` method. +- *id*: The unique identifier of the edge device for which the activation is being requested, locally is always `edge0`. +- *schedule*: An array of schedule items that define the activation pattern for the reserve. + +=== 2.1.6 Schedule Item Parameters + +Each schedule item within the schedule array specifies a specific activation period: + +- *startTimestamp*: The unix time stamp when the FFR should start activating. +- *duration*: The duration in milliseconds for which the reserve should remain active. +- *dischargePowerSetPoint*: The maximum power in kilowatts that the reserve should discharge during activation. +- *frequencyLimit*: The frequency threshold below which the reserve should be activated. +- *activationRunTime*: The time in milliseconds required for the reserve to fully activate. +- *supportDuration*: The time in milliseconds for which the reserve should continue providing support after the frequency has stabilized. + +=== 2.1.7 Example Python code + +[source,python] +---- +import requests +import json +from requests.auth import HTTPBasicAuth + +# API URL +url = 'http://10.0.10.178:8084/jsonrpc' + +# Authentication +auth = HTTPBasicAuth('x', 'owner') + +# Request headers +headers = { + 'Content-Type': 'application/json', +} + +# Request payload +payload = { + 'jsonrpc': '2.0', + 'id': '00000000-0000-0000-0000-000000000000', + 'method': 'componentJsonApi', + 'params': { + 'componentId': 'ctrlFastFreqReserve0', + 'payload': { + 'method': 'setActivateFastFreqReserve', + 'params': { + 'id': 'edge0', + 'schedule': [ + { + 'startTimestamp': 1701871562, + 'duration': 999, + 'dischargePowerSetPoint': 6000, + 'frequencyLimit': 502000, + 'activationRunTime': 'LONG_ACTIVATION_RUN', + 'supportDuration': 'LONG_SUPPORT_DURATION' + } + ] + } + } + } +} + +# Make the request +response = requests.post(url, auth=auth, headers=headers, json=payload) + +# Print the response +print(response.json()) +---- + + +== 3.1 REST API for Activating Fast Frequency Reserve controllers schedule using Backend +note : The controller/ App should be activated to update the schedule, which can be done using online monitoring or apache felix. + +== 3.1.1 Overview + +This REST API allows you to update FFR for a specific edge device. The API endpoint takes a JSON payload that updates activation schedule, including the start time, duration, discharge power set point, frequency limit, activation runtime, and support duration. + +== 3.1.2 Endpoint + +- *URL*: https://femecon.de/fems/rest/jsonrpc +- *Method*: POST +- *Content-Type*: application/json +- *Authorization*: Basic Authentication, username:foo.com, password:**** + +== 3.1.3 Body + +The request body must be a JSON object with the following structure: + +[source,json] +---- +{ + "method": "edgeRpc", + "params": { + "edgeId": "fems3734", + "payload": { + "method": "componentJsonApi", + "params": { + "componentId": "ctrlFastFreqReserve0", + "payload": { + "method": "setActivateFastFreqReserve", + "params": { + "id": "edge3734", + "schedule": [ + { + "startTimestamp": "1701767477", + "duration": "11000", + "dischargePowerSetPoint": "6000", + "frequencyLimit": "52000", + "activationRunTime": "LONG_ACTIVATION_RUN", + "supportDuration": "LONG_SUPPORT_DURATION" + } + ] + } + } + } + } + } +} +---- + +== 3.1.4 Request Parameters + +- *method*: The JSONRPC method to call. In this case, it is `edgeRpc`. +- *params*: The JSONRPC parameters. +- *edgeId*: The ID of the edge device for which to activate the FFR. +- *payload*: The JSONRPC payload. + +== 3.1.5 Payload Parameters + +Within the payload parameter, there is another JSON object that specifies the details of the activation request: + +- *method*: The JSONRPC method to call within the component. In this case, it is `componentJsonApi`. +- *params*: The JSONRPC parameters for the `componentJsonApi` method. +- *componentId*: The ID of the component within which to call the method. In this case, it is `ctrlFastFreqReserve0`. +- *payload*: The JSONRPC payload for the method. + +== 3.1.6 Schedule Item Parameters + +Each schedule item within the schedule array specifies a specific activation period: + +- *startTimestamp*: The unix time stamp in milliseconds when the FFR should start activating. +- *duration*: The duration in milliseconds for which the reserve should remain active. +- *dischargePowerSetPoint*: The maximum power in kilowatts that the reserve should discharge during activation. +- *frequencyLimit*: The frequency threshold below which the reserve should be activated. +- *activationRunTime*: The time in milliseconds required for the reserve to fully activate. +- *supportDuration*: The time in milliseconds for which the reserve should continue providing support after the frequency has stabilized. + +== 3.1.7 Example Python code: + +[source,python] +---- +import requests +import json +from requests.auth import HTTPBasicAuth +import base64 +import os + +url = "https://fenecon.de/fems/rest/jsonrpc" + +username = os.getenv("FENECON_USERNAME") +password = os.getenv("FENECON_PASSWORD") + +headers = { + "Content-Type": "application/json", + "Authorization": "Basic " + base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8") +} + +body = { + "method": "edgeRpc", + "params": { + "edgeId": "fems3734", + "payload": { + "method": "componentJsonApi", + "params": { + "componentId": "ctrlFastFreqReserve0", + "payload": { + "method": "setActivateFastFreqReserve", + "params": { + "id": "edge3734", + "schedule": [ + { + "startTimestamp": "1701767477", + "duration": "11000", + "dischargePowerSetPoint": "6000", + "frequencyLimit": "52000", + "activationRunTime": "LONG_ACTIVATION_RUN", + "supportDuration": "LONG_SUPPORT_DURATION" + } + ] + } + } + } + } + } +} + +response = requests.post(url, headers=headers, data=json.dumps(body)) + +if response.status_code == 200: + print("Fast Frequency Reserve activated successfully") +else: + print("Error activating Fast Frequency Reserve:", response.text) +---- + + +https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.ess.fastfrequencyreserve[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/Config.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/Config.java new file mode 100644 index 00000000000..2ca1b8b52f3 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/Config.java @@ -0,0 +1,54 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ControlMode; + +@ObjectClassDefinition(// + name = "Controller Ess Fast Frequency Reserve", // + description = "This Controller helps the energy storage system (ESS) generate capacity, essentially battery discharge. When the measured\n" + + "\"Grid frequency\" is lower than the predefined \"Frequency limit\".") // +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "ctrlFastFreqReserve0"; + + @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 = "Control Mode", description = "Set the type of control mode.") + ControlMode controlMode() default ControlMode.MANUAL_OFF; + + @AttributeDefinition(name = "Activation Schdule", description = "Schedule for the activation.") + String activationScheduleJson() default "[\n" // + + " {\n" // + + " \"startTimestamp\": 1684879200,\n" // + + " \"duration\": 86400,\n" // + + " \"dischargePowerSetPoint\": 92000,\n" // + + " \"frequencyLimit\": 49500,\n" // + + " \"activationRunTime\": \"LONG_ACTIVATION_RUN\",\n" // + + " \"supportDuration\": \"LONG_SUPPORT_DURATION\"\n" // + + " }\n" // + + "]"; // + + @AttributeDefinition(name = "Ess-ID", description = "ID of Ess device.") + String ess_id(); + + @AttributeDefinition(name = "Grid-Meter-Id", description = "ID of the Grid-Meter.") + String meter_id(); + + @AttributeDefinition(name = "Pre activation time", description = "A time before the activation time for charging the system(min).") + int preActivationTime() default 0; + + @AttributeDefinition(name = "Ess target filter", description = "This is auto-generated by 'Ess-ID'.") + String ess_target() default "(enabled=true)"; + + @AttributeDefinition(name = "Meter target filter", description = "This is auto-generated by 'Grid-Meter-ID'.") + String meter_target() default "(enabled=true)"; + + String webconsole_configurationFactory_nameHint() default "Controller Ess Fast Frequency Reserve [{id}]"; +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserve.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserve.java new file mode 100644 index 00000000000..efff694c2ee --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserve.java @@ -0,0 +1,283 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve; + +import io.openems.common.channel.AccessMode; +import io.openems.common.channel.Level; +import io.openems.common.channel.PersistencePriority; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Channel; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.StateChannel; +import io.openems.edge.common.channel.WriteChannel; +import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ActivationTime; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ControlMode; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.SupportDuration; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; + +public interface ControllerFastFrequencyReserve extends Controller, OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + CONTROL_MODE(Doc.of(ControlMode.values()) // + .initialValue(ControlMode.MANUAL_OFF) // + .text("Configured Control Mode")), // + STATE_MACHINE(Doc.of(State.values()) // + .persistencePriority(PersistencePriority.HIGH)// + .text("Current State of State-Machine")), // + SCHEDULE_PARSE_FAILED(Doc.of(Level.FAULT) // + .text("Unable to parse Schedule")), // + NO_ACTIVE_SETPOINT(Doc.of(OpenemsType.BOOLEAN) // + .text("No active Set-Point given")), // + DISCHARGE_POWER_SET_POINT(Doc.of(OpenemsType.INTEGER) // + .persistencePriority(PersistencePriority.HIGH)// + .accessMode(AccessMode.READ_WRITE)), + NO_FREQUENCY_LIMIT(Doc.of(OpenemsType.BOOLEAN) // + .text("No Frequency limit is given")), // + FREQUENCY_LIMIT(Doc.of(OpenemsType.INTEGER) // + .persistencePriority(PersistencePriority.HIGH)// + .accessMode(AccessMode.READ_WRITE)), // + NO_START_TIMESTAMP(Doc.of(OpenemsType.BOOLEAN) // + .text("No start timestamp")), // + START_TIMESTAMP(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH)// + .accessMode(AccessMode.READ_WRITE)), // + NO_DURATION(Doc.of(OpenemsType.BOOLEAN) // + .text("No duration")), // + DURATION(Doc.of(OpenemsType.INTEGER) // + .persistencePriority(PersistencePriority.HIGH)// + .accessMode(AccessMode.READ_WRITE)), // + ACTIVATION_TIME(Doc.of(ActivationTime.values())// + .accessMode(AccessMode.READ_WRITE)), // + SUPPORT_DURATIN(Doc.of(SupportDuration.values())// + .accessMode(AccessMode.READ_WRITE)), + LAST_TRIGGERED_TIME(Doc.of(OpenemsType.STRING) // + .persistencePriority(PersistencePriority.HIGH)// + .accessMode(AccessMode.READ_WRITE) // + .text("Last Triggered time in Human readable form")// + + ); + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + /** + * Gets the Channel for {@link ChannelId#SUPPORT_DURATIN}. + * + * @return the Channel + */ + public default Channel getSupportDurationChannel() { + return this.channel(ChannelId.SUPPORT_DURATIN); + } + + /** + * Gets the SupportDuration, see {@link ChannelId#SUPPORT_DURATIN}. + * + * @return the Channel {@link Value} + */ + public default SupportDuration getSupportDuration() { + return this.getSupportDurationChannel().value().asEnum(); + } + + /** + * Gets the Channel for {@link ChannelId#ACTIVATION_TIME}. + * + * @return the Channel + */ + public default Channel getActivationTimeChannel() { + return this.channel(ChannelId.ACTIVATION_TIME); + } + + /** + * Gets the ActivationTime, see {@link ChannelId#ACTIVATION_TIME}. + * + * @return the Channel {@link Value} + */ + public default ActivationTime getActivationTime() { + return this.getActivationTimeChannel().value().asEnum(); + } + + /** + * Gets the WriteChannel {@link ChannelId#LAST_TRIGGERED_TIME}. + * + * @return the WriteChannel + */ + public default WriteChannel getLastTriggeredTimeChannel() { + return this.channel(ChannelId.LAST_TRIGGERED_TIME); + } + + /** + * Gets the getLastTriggeredTime, see {@link ChannelId#LAST_TRIGGERED_TIME}. + * + * @return the Channel {@link Value} + */ + public default Value getLastTriggeredTime() { + return this.getLastTriggeredTimeChannel().value(); + } + + /** + * Sets the LastTriggeredTimseStamp, see {@link ChannelId#LAST_TRIGGERED_TIME}. + * + * @param value the value to be set + */ + public default void setLastTriggeredTime(String value) { + this.getLastTriggeredTimeChannel().setNextValue(value); + } + + /** + * Gets the Channel {@link ChannelId#SCHEDULE_PARSE_FAILED}. + * + * @return the Channel + */ + public default StateChannel getScheduleParseFailedChannel() { + return this.channel(ChannelId.SCHEDULE_PARSE_FAILED); + } + + /** + * Internal method to set the 'nextValue' on + * {@link ChannelId#SCHEDULE_PARSE_FAILED} Channel. + * + * @param value the next value + */ + public default void _setScheduleParseFailed(boolean value) { + this.getScheduleParseFailedChannel().setNextValue(value); + } + + /** + * Gets the Channel {@link ChannelId#DISCHARGE_POWER_SET_POINT}. + * + * @return the Channel + */ + public default WriteChannel getDischargePowerSetPointChannel() { + return this.channel(ChannelId.DISCHARGE_POWER_SET_POINT); + } + + /** + * Gets the getDischargeActivePowerSetPoint, see + * {@link ChannelId#DISCHARGE_POWER_SET_POINT}. + * + * @return the Channel {@link Value} + */ + public default Value getDischargePowerSetPoint() { + return this.getDischargePowerSetPointChannel().value(); + } + + /** + * Gets the WriteChannel {@link ChannelId#FREQUENCY_LIMIT}. + * + * @return the WriteChannel + */ + public default WriteChannel getFrequencyLimitChannel() { + return this.channel(ChannelId.FREQUENCY_LIMIT); + } + + /** + * Gets the getFrequencyLimit, see {@link ChannelId#FREQUENCY_LIMIT}. + * + * @return the Channel {@link Value} + */ + public default Value getFrequencyLimit() { + return this.getFrequencyLimitChannel().value(); + } + + /** + * Gets the WriteChannel {@link ChannelId#DURATION}. + * + * @return the WriteChannel + */ + public default WriteChannel getDurationChannel() { + return this.channel(ChannelId.DURATION); + } + + /* + * Gets the getDuration, see {@link ChannelId#DURATION}. + * + * @return the Channel {@link Value} + */ + public default Value getDuration() { + return this.getDurationChannel().value(); + } + + /** + * Gets the WriteChannel {@link ChannelId#START_TIMESTAMP}. + * + * @return the WriteChannel + */ + public default WriteChannel getStartTimestampChannel() { + return this.channel(ChannelId.START_TIMESTAMP); + } + + /** + * Gets the getStartTimestamp, see {@link ChannelId#START_TIMESTAMP}. + * + * @return the Channel {@link Value} + */ + public default Value getStartTimestamp() { + return this.getStartTimestampChannel().value(); + } + + /** + * Gets the Channel for {@link ChannelId#STATE_MACHINE}. + * + * @return the Channel + */ + public default Channel getStateMachineChannel() { + return this.channel(ChannelId.STATE_MACHINE); + } + + /** + * Gets the {@link StateChannel} for {@link ChannelId#STATE_MACHINE}. + * + * @return the Channel {@link Value} + */ + public default Value getStateMachine() { + return this.getStateMachineChannel().value(); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#STATE_MACHINE} + * Channel. + * + * @param value the next value + */ + public default void _setStateMachine(State value) { + this.getStateMachineChannel().setNextValue(value); + } + + /** + * Gets the Channel for {@link ChannelId#CONTROL_MODE}. + * + * @return the Channel + */ + public default Channel getControlModeChannel() { + return this.channel(ChannelId.CONTROL_MODE); + } + + /** + * Gets the {@link StateChannel} for {@link ChannelId#CONTROL_MODE}. + * + * @return the Channel {@link Value} + */ + public default Value getControlMode() { + return this.getControlModeChannel().value(); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#CONTROL_MODE} + * Channel. + * + * @param value the next value + */ + public default void _setControlMode(ControlMode value) { + this.getControlModeChannel().setNextValue(value); + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserveImpl.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserveImpl.java new file mode 100644 index 00000000000..5e9bf35f124 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserveImpl.java @@ -0,0 +1,312 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve; + +import java.time.Instant; +import java.time.ZoneId; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonArray; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.GenericJsonrpcResponseSuccess; +import io.openems.common.session.Role; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.channel.WriteChannel; +import io.openems.edge.common.channel.value.Value; +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.ComponentJsonApi; +import io.openems.edge.common.jsonapi.EdgeGuards; +import io.openems.edge.common.jsonapi.JsonApiBuilder; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ActivationTime; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.SupportDuration; +import io.openems.edge.controller.ess.fastfrequencyreserve.jsonrpc.SetActivateFastFreqReserveRequest; +import io.openems.edge.controller.ess.fastfrequencyreserve.jsonrpc.SetActivateFastFreqReserveRequest.ActivateFastFreqReserveSchedule; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.Context; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.meter.api.ElectricityMeter; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.Ess.FastFrequencyReserve", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class ControllerFastFrequencyReserveImpl extends AbstractOpenemsComponent + implements ControllerFastFrequencyReserve, Controller, OpenemsComponent, ComponentJsonApi { + + private final Logger log = LoggerFactory.getLogger(ControllerFastFrequencyReserveImpl.class); + private final StateMachine stateMachine = new StateMachine(State.UNDEFINED); + + private Config config = null; + private List schedule = new CopyOnWriteArrayList<>(); + + private static final Function OBTAIN_DICHARGE_POWER = ActivateFastFreqReserveSchedule::dischargePowerSetPoint; + private static final Function OBTAIN_FREQ_LIMIT = ActivateFastFreqReserveSchedule::frequencyLimit; + private static final Function OBTAIN_STARTTIME_STAMP = ActivateFastFreqReserveSchedule::startTimestamp; + private static final Function OBTAIN_DURATION = ActivateFastFreqReserveSchedule::duration; + private static final Function OBTAIN_ACTIVATION_TIME = ActivateFastFreqReserveSchedule::activationRunTime; + private static final Function OBTAIN_SUPPORT_DURATION = ActivateFastFreqReserveSchedule::supportDuration; + + @Reference + private ConfigurationAdmin cm; + + @Reference + private ComponentManager componentManager; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + private ManagedSymmetricEss ess; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + private ElectricityMeter meter; + + public ControllerFastFrequencyReserveImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + ControllerFastFrequencyReserve.ChannelId.values() // + ); + } + + @Activate + private void activate(ComponentContext context, Config config) throws OpenemsNamedException { + this.config = config; + super.activate(context, config.id(), config.alias(), config.enabled()); + this.updateConfig(); + } + + @Override + public void buildJsonApiRoutes(JsonApiBuilder builder) { + builder.handleRequest(SetActivateFastFreqReserveRequest.METHOD, // + endpoint -> { + endpoint.setGuards(EdgeGuards.roleIsAtleast(Role.OWNER)); + }, call -> { + this.handleSetActivateFastFreqReserveRequest( + SetActivateFastFreqReserveRequest.from(call.getRequest())); + + return new GenericJsonrpcResponseSuccess(call.getRequest().getId(), JsonUtils.buildJsonObject() // + .addProperty("startTimestamp", "recieved") // + .build()); + }); + } + + /** + * Updates the configuration for the component, setting control mode, + * references, and activation schedule. + * + * @throws OpenemsNamedException On Exception. + */ + private void updateConfig() throws OpenemsNamedException { + this._setControlMode(this.config.controlMode()); + + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", // + this.config.ess_id())) { + return; + } + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "meter", // + this.config.meter_id())) { + return; + } + try { + if (!this.config.activationScheduleJson().trim().isEmpty()) { + final var scheduleElement = JsonUtils.parse(this.config.activationScheduleJson()); + final var scheduleArray = JsonUtils.getAsJsonArray(scheduleElement); + this.applySchedule(scheduleArray); + this._setScheduleParseFailed(false); + } + } catch (IllegalStateException | OpenemsNamedException e) { + this._setScheduleParseFailed(true); + this.logError(this.log, "Unable to parse Schedule: " + e.getMessage()); + } + } + + /** + * Updates the configuration for activating fast frequency reserve based on the + * provided request. + * + * @param request The request containing the schedule information. + */ + private void updateConfig(SetActivateFastFreqReserveRequest request) { + var scheduleString = SetActivateFastFreqReserveRequest.listToString(request.getSchedule()); + OpenemsComponent.updateConfigurationProperty(this.cm, this.servicePid(), "activationScheduleJson", + scheduleString); + } + + private void applySchedule(JsonArray jsonArray) throws OpenemsNamedException { + this.schedule = SetActivateFastFreqReserveRequest.ActivateFastFreqReserveSchedule.from(jsonArray); + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public void run() throws OpenemsNamedException { + switch (this.config.controlMode()) { + case MANUAL_ON -> { + this.getConfigParams(); + this.handleStatemachine(); + } + case MANUAL_OFF -> { + // Do nothing + } + } + this._setControlMode(this.config.controlMode()); + } + + private void getConfigParams() { + + this.setChannelValueIfNeeded(ControllerFastFrequencyReserve.ChannelId.DISCHARGE_POWER_SET_POINT, + OBTAIN_DICHARGE_POWER); + final var channelValue = this.getDischargePowerSetPoint(); + + // Avoid calling + if (!channelValue.isDefined()) { + return; + } + this.setChannelValueIfNeeded(ControllerFastFrequencyReserve.ChannelId.FREQUENCY_LIMIT, // + OBTAIN_FREQ_LIMIT); + this.setChannelValueIfNeeded(ControllerFastFrequencyReserve.ChannelId.DURATION, // + OBTAIN_DURATION); + this.setChannelValueIfNeeded(ControllerFastFrequencyReserve.ChannelId.START_TIMESTAMP, // + OBTAIN_STARTTIME_STAMP); + // TODO get it for the activation time and support time, But currently this + // tested for long activation time and long support time, other enums are for + // future + this.setChannelValueIfNeeded(ControllerFastFrequencyReserve.ChannelId.ACTIVATION_TIME, // + OBTAIN_ACTIVATION_TIME); + this.setChannelValueIfNeeded(ControllerFastFrequencyReserve.ChannelId.SUPPORT_DURATIN, // + OBTAIN_SUPPORT_DURATION); + } + + /** + * Sets the value for the specified {@code FastFrequencyReserve.ChannelId} based + * on the provided {@code Function}, only if needed. + * + * @param channelId The channel to set the value for. + * @param obtainFunction A {@code Function} to retrieve the corresponding value + * based on the provided schedule entry. + */ + private void setChannelValueIfNeeded(ControllerFastFrequencyReserve.ChannelId channelId, + Function obtainFunction) { + WriteChannel channel = this.channel(channelId); + var setPointFromChannel = channel.value(); + if (setPointFromChannel.isDefined()) { + return; + } + + var currentTime = this.componentManager.getClock().withZone(ZoneId.systemDefault()); + var now = Instant.now(currentTime).getEpochSecond(); + + for (var scheduleEntry : this.schedule) { + var endTime = scheduleEntry.startTimestamp() + scheduleEntry.duration(); + + // Configurable minutes, and convert into seconds + var preActivationTimeBeforeStartTime = this.config.preActivationTime() * 60; + if (now >= scheduleEntry.startTimestamp() - preActivationTimeBeforeStartTime && now <= endTime) { + channel.setNextValue(obtainFunction.apply(scheduleEntry)); + return; + } + } + channel.setNextValue(null); + return; + } + + private void handleStatemachine() { + if (this.checkGridMode()) { + return; + } + + var state = this.stateMachine.getCurrentState(); + this._setStateMachine(state); + + if (!this.areChannelsDefined()) { + return; + } + + var context = new Context(this, // + this.componentManager.getClock(), // + this.ess, // + this.meter, // + this.getStartTimestamp().get(), // + this.getDuration().get(), // + this.getDischargePowerSetPoint().get(), // + this.getFrequencyLimit().get(), // + // TODO if other version of FFR needed, need to test first with the Inverter + // Capabilities + this.getActivationTime(), // + this.getSupportDuration()); + + try { + this.stateMachine.run(context); + } catch (OpenemsNamedException e) { + this.logError(this.log, "StateMachine failed: " + e.getMessage()); + } + } + + /** + * Checks the grid mode and returns a boolean value based on the grid mode + * state. If the grid mode is "ON_GRID" or "UNDEFINED," it returns false and + * logs a warning message when the grid mode is "UNDEFINED." If the grid mode is + * "OFF_GRID," it returns true. + * + * @return true if the grid mode is "OFF_GRID," false otherwise. + */ + private boolean checkGridMode() { + return switch (this.ess.getGridMode()) { + case ON_GRID -> false; + case UNDEFINED -> { + this.logWarn(this.log, "Grid-Mode is [UNDEFINED]"); + yield false; + } + case OFF_GRID -> true; + }; + } + + private boolean areChannelsDefined() { + return Stream.of(// + this.getDischargePowerSetPoint(), // + this.getFrequencyLimit(), // + this.getDuration(), // + this.getStartTimestamp())// + .allMatch(Value::isDefined); + } + + private void handleSetActivateFastFreqReserveRequest(SetActivateFastFreqReserveRequest request) + throws OpenemsNamedException { + this.schedule = request.getSchedule(); + + // get current schedule + var currentSchedule = (String) this.getComponentContext()// + .getProperties()// + .get("activationScheduleJson"); + var currentScheduleArray = JsonUtils.getAsJsonArray(JsonUtils.parse(currentSchedule).getAsJsonArray()); + var currentScheduleList = ActivateFastFreqReserveSchedule.from(currentScheduleArray); + + if (this.schedule.size() == currentScheduleArray.size() && currentScheduleList.equals(this.schedule)) { + return; + } + this.updateConfig(request); + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/enums/ActivationTime.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/enums/ActivationTime.java new file mode 100644 index 00000000000..5d483a3ef44 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/enums/ActivationTime.java @@ -0,0 +1,30 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.enums; + +import io.openems.common.types.OptionsEnum; + +public enum ActivationTime implements OptionsEnum { + SHORT_ACTIVATION_RUN(700, "Short activation time run, 700 in milliseconds"), // + MEDIUM_ACTIVATION_RUN(1000, "Medium activation time run, 1000 in milliseconds"), // + LONG_ACTIVATION_RUN(1300, "Long activation time run, 1300 in milliseconds"); + + private final int value; + private final String name; + + private ActivationTime(int value, String name) { + this.value = value; + this.name = name; + } + + public int getValue() { + return this.value; + } + + public String getName() { + return this.name; + } + + @Override + public OptionsEnum getUndefined() { + return LONG_ACTIVATION_RUN; + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/enums/ControlMode.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/enums/ControlMode.java new file mode 100644 index 00000000000..e203ad4afdd --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/enums/ControlMode.java @@ -0,0 +1,32 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.enums; + +import io.openems.common.types.OptionsEnum; + +public enum ControlMode implements OptionsEnum { + MANUAL_ON(0, "Manual control for the ON signal, FFR is swtiched on"), // + MANUAL_OFF(1, "Manual control for the OFF signal, FFR is swtiched off") // + ; // + + private final int value; + private final String name; + + private ControlMode(int value, String name) { + this.value = value; + this.name = name; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public OptionsEnum getUndefined() { + return MANUAL_OFF; + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/enums/SupportDuration.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/enums/SupportDuration.java new file mode 100644 index 00000000000..040806aa262 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/enums/SupportDuration.java @@ -0,0 +1,32 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.enums; + +import io.openems.common.types.OptionsEnum; + +public enum SupportDuration implements OptionsEnum { + SHORT_SUPPORT_DURATION(5, "long support duration 5 seconds"), + LONG_SUPPORT_DURATION(30, "long support duration 30 seconds"); + + private final int value; + private final String name; + + private SupportDuration(int value, String name) { + this.value = value; + this.name = name; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public OptionsEnum getUndefined() { + return LONG_SUPPORT_DURATION; + } + +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/jsonrpc/SetActivateFastFreqReserveRequest.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/jsonrpc/SetActivateFastFreqReserveRequest.java new file mode 100644 index 00000000000..4c422a365bc --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/jsonrpc/SetActivateFastFreqReserveRequest.java @@ -0,0 +1,228 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.jsonrpc; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ActivationTime; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.SupportDuration; + +/** + * Represents a JSON-RPC Request for 'setActivateFastFreqReserve'. + * + *

+ {
+ 	"jsonrpc": "2.0",
+ 	"id": "UUID",
+ 	"method": "setActivateFastFreqReserve",
+ 	"params": { 		
+ 		"schedule": [{
+ 			"startTimestamp": 1542464697,
+ 			"duration": 900,
+ 			"dischargeActivePowerSetPoint": 92000,
+ 			"frequencyLimit": 49500
+ 			"activationRunTime": "LONG_ACTIVATION_RUN",
+ 			"supportDuration": "LONG_SUPPORT_DURATION"
+ 		}]
+ 	}
+ }
+ * 
+ */ +public class SetActivateFastFreqReserveRequest extends JsonrpcRequest { + + /** + * Create {@link SetActivateFastFreqReserveRequest} from a template + * {@link JsonrpcRequest}. + * + * @param request the template {@link JsonrpcRequest} + * @return the {@link SetActivateFastFreqReserveRequest} + * @throws OpenemsNamedException on parse error + */ + public static SetActivateFastFreqReserveRequest from(JsonrpcRequest request) throws OpenemsNamedException { + final var params = request.getParams(); + final var edgeId = JsonUtils.getAsString(params, "id"); + final var scheduleArray = JsonUtils.getAsJsonArray(params, "schedule"); + final var schedule = ActivateFastFreqReserveSchedule.from(scheduleArray); + return new SetActivateFastFreqReserveRequest(request, edgeId, schedule); + } + + public static final String METHOD = "setActivateFastFreqReserve"; + + private final String edgeId; + private final List schedule; + + public SetActivateFastFreqReserveRequest(String edgeId) { + this(edgeId, new ArrayList<>()); + } + + private SetActivateFastFreqReserveRequest(String edgeId, List schedule) { + super(SetActivateFastFreqReserveRequest.METHOD); + this.edgeId = edgeId; + this.schedule = schedule; + } + + private SetActivateFastFreqReserveRequest(JsonrpcRequest request, String edgeId, + List schedule) { + super(request, SetActivateFastFreqReserveRequest.METHOD); + this.edgeId = edgeId; + this.schedule = schedule; + } + + /** + * Adds a new schedule entry for activating Fast Frequency Reserve. + * + * @param scheduleEntry The schedule entry to be added. + */ + public void addScheduleEntry(ActivateFastFreqReserveSchedule scheduleEntry) { + this.schedule.add(scheduleEntry); + } + + @Override + public JsonObject getParams() { + var schedule = new JsonArray(); + for (var se : this.schedule) { + schedule.add(se.toJson()); + } + return JsonUtils.buildJsonObject() // + .addProperty("id", this.getEdgeId()) // + .add("schedule", schedule) // + .build(); + } + + /** + * Gets the Edge-ID. + * + * @return Edge-ID + */ + public String getEdgeId() { + return this.edgeId; + } + + public List getSchedule() { + return this.schedule; + } + + /** + * Converts a list of ActivateFastFreqReserveSchedule objects to a formatted + * string. + * + * @param scheduleList The list of ActivateFastFreqReserveSchedule objects to + * convert. + * @return A string representation of the schedule list. + * @see ActivateFastFreqReserveSchedule#toString() + */ + public static String listToString(List scheduleList) { + return "["// + + scheduleList.stream()// + .map(ActivateFastFreqReserveSchedule::toString)// + .collect(Collectors.joining(", "))// + + "]"; + } + + public record ActivateFastFreqReserveSchedule(long startTimestamp, int duration, int dischargePowerSetPoint, + int frequencyLimit, ActivationTime activationRunTime, SupportDuration supportDuration) { + + /** + * Builds a list of ActivateFastFreqReserveSchedule from a JsonArray. + * + * @param jsonArray JsonArray + * @return list of {@link ActivateFastFreqReserveSchedule} + * @throws OpenemsNamedException on error + */ + public static List from(JsonArray jsonArray) throws OpenemsNamedException { + List schedule = new ArrayList<>(); + for (var jsonElement : jsonArray) { + var newSchedule = new ActivateFastFreqReserveSchedule( + JsonUtils.getAsLong(jsonElement, "startTimestamp"), // + JsonUtils.getAsInt(jsonElement, "duration"), + JsonUtils.getAsInt(jsonElement, "dischargePowerSetPoint"), + JsonUtils.getAsInt(jsonElement, "frequencyLimit"), + JsonUtils.getAsEnum(ActivationTime.class, jsonElement, "activationRunTime"), + JsonUtils.getAsEnum(SupportDuration.class, jsonElement, "supportDuration")); + + // Check for overlap with existing schedules before adding + if (!overlapsExistingSchedule(schedule, newSchedule)) { + schedule.add(newSchedule); + } + } + + schedule.sort(Comparator.comparing(ActivateFastFreqReserveSchedule::startTimestamp)); + return schedule; + } + + /** + * Checks whether a new schedule overlaps with existing schedules or is an exact + * duplicate. + * + * @param schedule List of existing schedules to compare against + * @param newSchedule The new schedule to check for overlap or duplication + * @return {@code true} if the new schedule overlaps with existing schedules or + * is an exact duplicate, {@code false} otherwise + */ + private static boolean overlapsExistingSchedule(List schedule, + ActivateFastFreqReserveSchedule newSchedule) { + for (ActivateFastFreqReserveSchedule existingSchedule : schedule) { + if (newSchedule.equals(existingSchedule)) { + // duplicate found + return true; + } + // Check for overlap + if (newSchedule.startTimestamp < (existingSchedule.startTimestamp + existingSchedule.duration) + && (newSchedule.startTimestamp + newSchedule.duration) > existingSchedule.startTimestamp) { + return true; + } + } + // No overlap or exact duplicate found + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + var that = (ActivateFastFreqReserveSchedule) o; + return this.startTimestamp == that.startTimestamp // + && this.duration == that.duration // + && this.dischargePowerSetPoint == that.dischargePowerSetPoint // + && this.frequencyLimit == that.frequencyLimit // + && this.activationRunTime.equals(that.activationRunTime) // + && this.supportDuration.equals(that.supportDuration); + } + + @Override + public String toString() { + return String.format( + "{\"startTimestamp\":%d, \"duration\":%d, \"dischargePowerSetPoint\":%d, \"frequencyLimit\":%d, \"activationRunTime\":\"%s\", \"supportDuration\":\"%s\"}", + this.startTimestamp, this.duration, this.dischargePowerSetPoint, this.frequencyLimit, + this.activationRunTime, this.supportDuration); + } + + /** + * Converts this ActivateFastFreqReserveSchedule object to a JsonObject. + * + * @return A JsonObject representing this schedule, where each field is mapped + * to a corresponding property with its value. + */ + public JsonObject toJson() { + return JsonUtils.buildJsonObject() // + .addProperty("startTimestamp", this.startTimestamp()) // + .addProperty("duration", this.duration()) // + .addProperty("dischargeActivePowerSetPoint", this.dischargePowerSetPoint()) // + .addProperty("frequencyLimit", this.frequencyLimit()) // + .addProperty("activationRunTime", this.activationRunTime()) // + .addProperty("supportDuration", this.supportDuration()) // + .build(); + } + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/ActivationTimeHandler.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/ActivationTimeHandler.java new file mode 100644 index 00000000000..57af0c89ba9 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/ActivationTimeHandler.java @@ -0,0 +1,106 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.statemachine; + +import java.time.Duration; +import java.time.Instant; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.utils.EnumUtils; +import io.openems.edge.common.statemachine.StateHandler; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; + +public class ActivationTimeHandler extends StateHandler { + + private static final int ZERO_WATT_POWER = 0; // [0 W] + + protected Instant dipDetectedStartTime; + protected ActivationTimeState activationTimeState; + + private static enum SubState { + INSIDE_TIME_FRAME, // + HANDLE_WAITING_FREQ_DIP, // + HANDLE_FREQ_DIP, // + FINISH_ACTIVATION + } + + protected static record ActivationTimeState(SubState subState, Instant lastChange) { + } + + @Override + protected void onEntry(Context context) throws OpenemsNamedException { + context.ess.setActivePowerEquals(ZERO_WATT_POWER); + this.activationTimeState = new ActivationTimeState(SubState.INSIDE_TIME_FRAME, Instant.now(context.clock)); + } + + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + final var nextSubState = this.getNextSubState(context); + if (nextSubState != this.activationTimeState.subState) { + this.activationTimeState = new ActivationTimeState(nextSubState, Instant.now(context.clock)); + } + if (nextSubState == SubState.FINISH_ACTIVATION) { + return State.SUPPORT_DURATION; + } + + return State.ACTIVATION_TIME; + } + + private SubState getNextSubState(Context context) throws OpenemsNamedException { + return switch (this.activationTimeState.subState) { + case INSIDE_TIME_FRAME -> + this.isInsideTimeFrame(context) ? SubState.FINISH_ACTIVATION : SubState.HANDLE_WAITING_FREQ_DIP; + case HANDLE_WAITING_FREQ_DIP -> { + if (this.isFrequencyDipped(context)) { + context.ess.setActivePowerEquals(context.dischargePower); + var time = Instant.now(context.clock); + this.clockActivationTime(context, time); + this.dipDetectedStartTime = time; + yield SubState.HANDLE_FREQ_DIP; + } + context.ess.setActivePowerEquals(ZERO_WATT_POWER); + yield SubState.HANDLE_WAITING_FREQ_DIP; + } + case HANDLE_FREQ_DIP -> { + context.ess.setActivePowerEquals(context.dischargePower); + var activationExpirationTime = Duration.between(this.dipDetectedStartTime, Instant.now(context.clock))// + .toMillis(); // + if (activationExpirationTime >= context.activationRunTime.getValue()) { + yield SubState.FINISH_ACTIVATION; + } + yield SubState.HANDLE_FREQ_DIP; + } + case FINISH_ACTIVATION -> SubState.FINISH_ACTIVATION; + }; + } + + /** + * Clocks the activation time and sets it in the context. + * + * @param context the context. + * @param time time in instant + */ + private void clockActivationTime(Context context, Instant time) { + context.setCycleStart(time); + } + + private boolean isFrequencyDipped(Context context) throws OpenemsException { + var meterFrequency = context.meter.getFrequency(); + if (!meterFrequency.isDefined()) { + throw new OpenemsException("meter has no frequency channel defined."); + } + return (meterFrequency.get() < context.freqLimit); + } + + private boolean isInsideTimeFrame(Context context) { + final var now = Instant.now(context.clock).getEpochSecond(); + final var startTimestamp = context.startTimestamp; + final var duration = context.duration; + return now >= startTimestamp + duration; + } + + @Override + protected String debugLog() { + return State.ACTIVATION_TIME.asCamelCase() + "-" + + EnumUtils.nameAsCamelCase(this.activationTimeState.subState()); + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/BufferedTimeBeforeRecoveryHandler.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/BufferedTimeBeforeRecoveryHandler.java new file mode 100644 index 00000000000..23d9ae25b3d --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/BufferedTimeBeforeRecoveryHandler.java @@ -0,0 +1,103 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.statemachine; + +import java.time.Duration; +import java.time.Instant; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.utils.EnumUtils; +import io.openems.edge.common.statemachine.StateHandler; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.ess.power.api.Phase; +import io.openems.edge.ess.power.api.Pwr; + +public class BufferedTimeBeforeRecoveryHandler extends StateHandler { + + private static final int ZERO_WATT_POWER = 0; + private static final int BUFFER_DURATION_THRESHOLD_SECONDS = 15; // [s] + private static final int RECOVERY_DURATION_THRESHOLD_MINUTES = 4; // [minute] + private static final double EIGHTEENX_PERCENT_OF_MAX_POWER = 0.18; // [%] + + protected Instant bufferedTimeBeforeRecoveryStartTime = Instant.MIN; + + protected static record BufferedTimeBeforeRecoveryState(SubState subState, Instant lastChange) { + } + + protected BufferedTimeBeforeRecoveryState bufferedTimeBeforeRecoveryState; + + private static enum SubState { + HOLD_BUFFERED_TIME_BEFORE_RECOVERY, // + BUFFERED_TIME_RECOVERY, // + FINISH_BUFFERED_TIME_BEFORE_RECOVERY + } + + @Override + protected void onEntry(Context context) throws OpenemsNamedException { + context.ess.setActivePowerEquals(ZERO_WATT_POWER); + final var now = Instant.now(context.clock); + this.bufferedTimeBeforeRecoveryStartTime = now; + this.bufferedTimeBeforeRecoveryState = new BufferedTimeBeforeRecoveryState( + SubState.HOLD_BUFFERED_TIME_BEFORE_RECOVERY, now); + + } + + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + final var nextSubState = this.getNextSubState(context); + + if (nextSubState != this.bufferedTimeBeforeRecoveryState.subState) { + this.bufferedTimeBeforeRecoveryState = new BufferedTimeBeforeRecoveryState(nextSubState, + Instant.now(context.clock)); + } + + if (nextSubState == SubState.FINISH_BUFFERED_TIME_BEFORE_RECOVERY) { + return State.RECOVERY_TIME; + } + + return State.BUFFERED_TIME_BEFORE_RECOVERY; + } + + private SubState getNextSubState(Context context) throws OpenemsNamedException { + return switch (this.bufferedTimeBeforeRecoveryState.subState) { + case HOLD_BUFFERED_TIME_BEFORE_RECOVERY -> { + context.ess.setActivePowerEquals(ZERO_WATT_POWER); + var bufferedDurationExpiration = this.calculateBufferedDurationExpiration(context); + if (bufferedDurationExpiration >= BUFFER_DURATION_THRESHOLD_SECONDS) { + yield SubState.BUFFERED_TIME_RECOVERY; + } + yield SubState.HOLD_BUFFERED_TIME_BEFORE_RECOVERY; + } + case BUFFERED_TIME_RECOVERY -> { + var minPowerEss = this.calculateMinPower(context.ess); + context.ess.setActivePowerEquals(minPowerEss); + var bufferedRecoveryExpiration = this.calculateBufferedRecoveryExpiration(context); + if (bufferedRecoveryExpiration >= RECOVERY_DURATION_THRESHOLD_MINUTES) { + yield SubState.FINISH_BUFFERED_TIME_BEFORE_RECOVERY; + } + yield SubState.BUFFERED_TIME_RECOVERY; + } + case FINISH_BUFFERED_TIME_BEFORE_RECOVERY -> SubState.FINISH_BUFFERED_TIME_BEFORE_RECOVERY; + }; + } + + private long calculateBufferedDurationExpiration(Context context) { + return Duration.between(this.bufferedTimeBeforeRecoveryStartTime, Instant.now(context.clock))// + .toSeconds(); + } + + private long calculateBufferedRecoveryExpiration(Context context) { + return Duration// + .between(context.getCycleStart(), Instant.now(context.clock))// + .toMinutes(); + } + + private int calculateMinPower(ManagedSymmetricEss ess) { + return (int) (ess.getPower().getMinPower(ess, Phase.ALL, Pwr.ACTIVE) * EIGHTEENX_PERCENT_OF_MAX_POWER); + } + + @Override + protected String debugLog() { + return State.BUFFERED_TIME_BEFORE_RECOVERY.asCamelCase() + "-" + + EnumUtils.nameAsCamelCase(this.bufferedTimeBeforeRecoveryState.subState()); + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/Context.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/Context.java new file mode 100644 index 00000000000..cbae7c9ec6e --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/Context.java @@ -0,0 +1,64 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.statemachine; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import io.openems.edge.common.statemachine.AbstractContext; +import io.openems.edge.controller.ess.fastfrequencyreserve.ControllerFastFrequencyReserve; +import io.openems.edge.controller.ess.fastfrequencyreserve.ControllerFastFrequencyReserveImpl; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ActivationTime; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.SupportDuration; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.meter.api.ElectricityMeter; + +public class Context extends AbstractContext { + + protected final Clock clock; + protected final int dischargePower; + protected final long startTimestamp; + protected final int duration; + protected final int freqLimit; + protected final ActivationTime activationRunTime; + protected final SupportDuration supportDuration; + protected final ControllerFastFrequencyReserve parentController; + protected final ManagedSymmetricEss ess; + protected final ElectricityMeter meter; + + protected static Instant _cycleStart; + + public Context(ControllerFastFrequencyReserve fastFrequencyReserve, // + Clock clock, // + ManagedSymmetricEss ess, // + ElectricityMeter meter, // + long startTimestamp, // + int duration, // + int dischargePower, // + int freqLimit, // + ActivationTime activationRunTime, // + SupportDuration supportDuration) { + this.clock = clock; + this.parentController = fastFrequencyReserve; + this.startTimestamp = startTimestamp; + this.duration = duration; + this.dischargePower = dischargePower; + this.freqLimit = freqLimit; + this.ess = ess; + this.meter = meter; + this.activationRunTime = activationRunTime; + this.supportDuration = supportDuration; + } + + public Instant getCycleStart() { + return _cycleStart; + } + + public void setCycleStart(Instant cycleStart) { + LocalDateTime lastTriggered = LocalDateTime.ofInstant(cycleStart, ZoneId.systemDefault()); + String formattedDateTime = lastTriggered.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + this.parentController.setLastTriggeredTime(formattedDateTime); + _cycleStart = cycleStart; + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/DeactivationTimeHandler.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/DeactivationTimeHandler.java new file mode 100644 index 00000000000..aa01cbddbbb --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/DeactivationTimeHandler.java @@ -0,0 +1,81 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.statemachine; + +import java.time.Duration; +import java.time.Instant; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.utils.EnumUtils; +import io.openems.edge.common.statemachine.StateHandler; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; + +public class DeactivationTimeHandler extends StateHandler { + + private static final int ZERO_WATT_POWER = 0; //[0 W] + protected Instant deactivationStateStartTime; + + private static enum SubState { + HOLD_DEACTIVATION, // + FINISH_DEACTIVATION_DURATION + } + + protected static record DeactivationTimeState(SubState subState, Instant lastChange) { + } + + protected DeactivationTimeState deactivationTimeState; + + @Override + protected void onEntry(Context context) throws OpenemsNamedException { + context.ess.setActivePowerEquals(ZERO_WATT_POWER); + this.deactivationStateStartTime = Instant.now(context.clock); + this.deactivationTimeState = new DeactivationTimeState(SubState.HOLD_DEACTIVATION, Instant.now(context.clock)); + } + + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + var nextSubState = this.getNextSubState(context); + if (nextSubState != this.deactivationTimeState.subState) { + this.deactivationTimeState = new DeactivationTimeState(nextSubState, Instant.now(context.clock)); + } + if (nextSubState == SubState.FINISH_DEACTIVATION_DURATION) { + return State.BUFFERED_TIME_BEFORE_RECOVERY; + } + return State.DEACTIVATION_TIME; + } + + private SubState getNextSubState(Context context) throws OpenemsNamedException { + return switch (this.deactivationTimeState.subState) { + + case HOLD_DEACTIVATION -> { + context.ess.setActivePowerEquals(ZERO_WATT_POWER); + var deactivationDurationExpiration = this.calculateDeactivationDurationExpiration(context); + if (deactivationDurationExpiration >= context.activationRunTime.getValue()) { + yield SubState.FINISH_DEACTIVATION_DURATION; + } + yield SubState.HOLD_DEACTIVATION; + } + case FINISH_DEACTIVATION_DURATION -> SubState.FINISH_DEACTIVATION_DURATION; + }; + } + + /** + * Calculates the expiration duration for the deactivation state. The expiration + * duration is the time elapsed between the deactivation state start time and + * the current time, measured in milliseconds. + * + * @param context the Context + * @return The expiration duration in milliseconds. + */ + private long calculateDeactivationDurationExpiration(Context context) { + return Duration.between(// + this.deactivationStateStartTime, // + Instant.now(context.clock))// + .toMillis(); + } + + @Override + protected String debugLog() { + return State.DEACTIVATION_TIME.asCamelCase() + "-" + + EnumUtils.nameAsCamelCase(this.deactivationTimeState.subState()); + } + +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/PreActivationHandler.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/PreActivationHandler.java new file mode 100644 index 00000000000..86b9d62083e --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/PreActivationHandler.java @@ -0,0 +1,53 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.statemachine; + +import java.time.ZonedDateTime; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.statemachine.StateHandler; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.ess.power.api.Phase; +import io.openems.edge.ess.power.api.Pwr; + +public class PreActivationHandler extends StateHandler { + + private static final double EIGHTEENX_PERCENT_OF_MAX_POWER = 0.18; // [%] + + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + var ess = context.ess; + int minPowerEss = this.calculateMinPower(ess); + + if (this.isActivationTime(context)) { + return State.ACTIVATION_TIME; + } else { + ess.setActivePowerEquals(minPowerEss); + return State.PRE_ACTIVATION_STATE; + } + } + + /** + * Calculates 18% of the minimum power of the given ess. + * + * @param ess The managed symmetric ess. + * @return 18% of the minimum power of the ess. + */ + private int calculateMinPower(ManagedSymmetricEss ess) { + return (int) (ess.getPower().getMinPower(ess, Phase.ALL, Pwr.ACTIVE) * EIGHTEENX_PERCENT_OF_MAX_POWER); + } + + /** + * Checks if the current time, as adjusted by the component manager's clock, is + * within the activation time window. + * + * + * @param context the context + * @return {@code true} if the current time is within the activation time + * window, {@code false} otherwise. + */ + private boolean isActivationTime(Context context) { + var currentEpochSecond = ZonedDateTime.now(context.clock).toEpochSecond(); + return currentEpochSecond >= context.startTimestamp + && currentEpochSecond <= context.startTimestamp + context.duration; + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/RecoveryTimeHandler.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/RecoveryTimeHandler.java new file mode 100644 index 00000000000..d073c55f9cf --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/RecoveryTimeHandler.java @@ -0,0 +1,29 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.statemachine; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; + +import io.openems.edge.common.statemachine.StateHandler; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; + +public class RecoveryTimeHandler extends StateHandler { + + public static final int RECOVERY_DURATION_SECONDS = 15 * 60; + + @Override + protected State runAndGetNextState(Context context) { + if (this.isItWithinDuration(context)) { + return State.ACTIVATION_TIME; + } + return State.RECOVERY_TIME; + } + + private boolean isItWithinDuration(Context context) { + var now = Instant.now(context.clock).getEpochSecond(); + var expiration = Duration// + .between(context.getCycleStart(), ZonedDateTime.now(context.clock))// + .toSeconds(); + return expiration > RECOVERY_DURATION_SECONDS || now >= context.startTimestamp + context.duration; + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/StateMachine.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/StateMachine.java new file mode 100644 index 00000000000..c044760a26f --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/StateMachine.java @@ -0,0 +1,62 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.statemachine; + +import io.openems.common.types.OptionsEnum; +import io.openems.edge.common.statemachine.AbstractStateMachine; +import io.openems.edge.common.statemachine.StateHandler; + +public class StateMachine extends AbstractStateMachine { + + public enum State implements io.openems.edge.common.statemachine.State, OptionsEnum { + UNDEFINED(-1), // + PRE_ACTIVATION_STATE(10), // + ACTIVATION_TIME(20), // + SUPPORT_DURATION(30), // + DEACTIVATION_TIME(40), // + BUFFERED_TIME_BEFORE_RECOVERY(50), // + RECOVERY_TIME(60);// + + private final int value; + + private State(int value) { + this.value = value; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public OptionsEnum getUndefined() { + return UNDEFINED; + } + + @Override + public String getName() { + return this.name(); + } + + @Override + public State[] getStates() { + return State.values(); + } + + } + + public StateMachine(State initialState) { + super(initialState); + } + + @Override + public StateHandler getStateHandler(State state) { + return switch (state) { + case ACTIVATION_TIME -> new ActivationTimeHandler(); + case BUFFERED_TIME_BEFORE_RECOVERY -> new BufferedTimeBeforeRecoveryHandler(); + case DEACTIVATION_TIME -> new DeactivationTimeHandler(); + case PRE_ACTIVATION_STATE -> new PreActivationHandler(); + case RECOVERY_TIME -> new RecoveryTimeHandler(); + case SUPPORT_DURATION -> new SupportDurationTimeHandler(); + case UNDEFINED -> new UndefinedHandler(); + }; + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/SupportDurationTimeHandler.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/SupportDurationTimeHandler.java new file mode 100644 index 00000000000..a5d5131c8e0 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/SupportDurationTimeHandler.java @@ -0,0 +1,64 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.statemachine; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.statemachine.StateHandler; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; + +public class SupportDurationTimeHandler extends StateHandler { + + protected LocalDateTime supportDurationStartTime; + + private static enum SubState { + HOLD_SUPPORT, // + FINISH_SUPPORT_DURATION + } + + protected static record SupportDurationTimeState(SubState subState, Instant lastChange) { + } + + protected SupportDurationTimeState supportDurationTimeState; + + @Override + protected void onEntry(Context context) throws OpenemsNamedException { + context.ess.setActivePowerEquals(context.dischargePower); + this.supportDurationStartTime = LocalDateTime.now(context.clock); + this.supportDurationTimeState = new SupportDurationTimeState(SubState.HOLD_SUPPORT, Instant.now(context.clock)); + } + + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + final var nextSubState = this.getNextSubState(context); + if (nextSubState != this.supportDurationTimeState.subState) { + this.supportDurationTimeState = new SupportDurationTimeState(nextSubState, Instant.now(context.clock)); + } + if (nextSubState == SubState.FINISH_SUPPORT_DURATION) { + return State.DEACTIVATION_TIME; + } + return State.SUPPORT_DURATION; + } + + private SubState getNextSubState(Context context) throws OpenemsNamedException { + return switch (this.supportDurationTimeState.subState) { + case HOLD_SUPPORT -> { + context.ess.setActivePowerEquals(context.dischargePower); + var supportDurationExpiration = this.calculateSupportDurationExpiration(context); + if (supportDurationExpiration >= context.supportDuration.getValue()) { + yield SubState.FINISH_SUPPORT_DURATION; + } + yield SubState.HOLD_SUPPORT; + } + case FINISH_SUPPORT_DURATION -> SubState.FINISH_SUPPORT_DURATION; + }; + } + + private long calculateSupportDurationExpiration(Context context) { + return Duration.between(// + this.supportDurationStartTime, // + LocalDateTime.now(context.clock))// + .getSeconds(); + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/UndefinedHandler.java b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/UndefinedHandler.java new file mode 100644 index 00000000000..513a119914a --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/src/io/openems/edge/controller/ess/fastfrequencyreserve/statemachine/UndefinedHandler.java @@ -0,0 +1,40 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve.statemachine; + +import java.time.ZonedDateTime; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.statemachine.StateHandler; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; + +public class UndefinedHandler extends StateHandler { + + public static final int FIFTEEN_MINUTES_IN_SECONDS = 15 * 60; // 15 minutes in seconds + + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + if (this.isPreActivationTime(context)) { + return State.PRE_ACTIVATION_STATE; + } else { + return State.UNDEFINED; + } + } + + /** + * Checks if the current time, as adjusted by the component manager's clock, is + * within the pre-activation time window. pre-activation time window is 15 + * minutes before the start time. + * + * @param context the context + * @return {@code true} if the current time is within the activation time + * window, {@code false} otherwise. + */ + private boolean isPreActivationTime(Context context) { + var currentDateTime = ZonedDateTime.now(context.clock); + var currentEpochSecond = currentDateTime.toEpochSecond(); + if (currentEpochSecond >= context.startTimestamp - FIFTEEN_MINUTES_IN_SECONDS + && currentEpochSecond <= context.startTimestamp + context.duration) { + return true; + } + return false; + } +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/test/.gitignore b/io.openems.edge.controller.ess.fastfrequencyreserve/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserveImplTest.java b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserveImplTest.java new file mode 100644 index 00000000000..e704c552b50 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserveImplTest.java @@ -0,0 +1,284 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve; + +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.controller.ess.fastfrequencyreserve.ControllerFastFrequencyReserve.ChannelId.STATE_MACHINE; +import static io.openems.edge.controller.ess.fastfrequencyreserve.enums.ActivationTime.LONG_ACTIVATION_RUN; +import static io.openems.edge.controller.ess.fastfrequencyreserve.enums.SupportDuration.LONG_SUPPORT_DURATION; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.FREQUENCY; +import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.SECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; + +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.test.TimeLeapClock; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.sum.GridMode; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ControlMode; +import io.openems.edge.controller.ess.fastfrequencyreserve.jsonrpc.SetActivateFastFreqReserveRequest.ActivateFastFreqReserveSchedule; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; +import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.meter.test.DummyElectricityMeter; + +public class ControllerFastFrequencyReserveImplTest { + + @Test + public void testFfrController() throws Exception { + final var clock = new TimeLeapClock(Instant.parse("2023-07-13T08:45:00.00Z"), ZoneOffset.UTC); + new ControllerTest(new ControllerFastFrequencyReserveImpl()) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedSymmetricEss("ess0") // + .withGridMode(GridMode.ON_GRID)// + .withMaxApparentPower(92000)// + .withAllowedChargePower(-92000)// + .withAllowedDischargePower(92000)) // + .addReference("meter", new DummyElectricityMeter("meter0")) // + .activate(MyConfig.create() // + .setEssId("ess0") // + .setId("ctrl0") // + .setMeterId("meter0") // + .setMode(ControlMode.MANUAL_ON) // + .setPreActivationTime(15)// + .setactivationScheduleJson(JsonUtils.buildJsonArray() // + .add(JsonUtils.buildJsonObject() // + .addProperty("startTimestamp", 1689242400) // Thu Jul 13 2023 10:00:00 GMT+0000 + .addProperty("duration", 86400) // + .addProperty("dischargePowerSetPoint", 92000) // + .addProperty("frequencyLimit", 49500) // + .addProperty("activationRunTime", LONG_ACTIVATION_RUN) // + .addProperty("supportDuration", LONG_SUPPORT_DURATION) // + .build()) + .add(JsonUtils.buildJsonObject() // + .addProperty("startTimestamp", 1689331500) // Fri Jul 14 2023 10:45:00 GMT+0000 + .addProperty("duration", 86400) // + .addProperty("dischargePowerSetPoint", 92000) // + .addProperty("frequencyLimit", 49500) // + .addProperty("activationRunTime", LONG_ACTIVATION_RUN) // + .addProperty("supportDuration", LONG_SUPPORT_DURATION) // + .build()) + .build()// + .toString())// + .build()) + .next(new TestCase("1") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.UNDEFINED) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, null))// + .next(new TestCase("2") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.UNDEFINED) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, null))// + .next(new TestCase("3") // + .timeleap(clock, 1, HOURS) // + .input("meter0", FREQUENCY, 50000))// + .next(new TestCase("4"))// + .next(new TestCase("5") // + .output(STATE_MACHINE, State.UNDEFINED)) + .next(new TestCase("6") // + .timeleap(clock, 10, MINUTES)// + .output(STATE_MACHINE, State.PRE_ACTIVATION_STATE)) + .next(new TestCase("7") // + .timeleap(clock, 10, MINUTES)) + .next(new TestCase("8") // + .output(STATE_MACHINE, State.ACTIVATION_TIME)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("9") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.ACTIVATION_TIME)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("10") // + .input("meter0", FREQUENCY, 49400)// + .output(STATE_MACHINE, State.ACTIVATION_TIME)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 92000)) + .next(new TestCase("11") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("12") // + .output(STATE_MACHINE, State.SUPPORT_DURATION)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 92000)) // + .next(new TestCase("13") // + .timeleap(clock, LONG_SUPPORT_DURATION.getValue(), SECONDS) // + .output(STATE_MACHINE, State.SUPPORT_DURATION)) + .next(new TestCase("14") // + .output(STATE_MACHINE, State.DEACTIVATION_TIME) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("15") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("16") // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("17") // + .timeleap(clock, 16, SECONDS) // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY)) + .next(new TestCase("18") // + .timeleap(clock, 12, MINUTES)) // + .next(new TestCase("19") // + .output(STATE_MACHINE, State.RECOVERY_TIME) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -16560)) + .next(new TestCase("20") // + .output(STATE_MACHINE, State.RECOVERY_TIME)) + .next(new TestCase("21") // + .output(STATE_MACHINE, State.RECOVERY_TIME)) + .next(new TestCase("22") // + .timeleap(clock, LONG_SUPPORT_DURATION.getValue(), MINUTES)) + .next(new TestCase("23") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.ACTIVATION_TIME)) + .next(new TestCase("24") // + .timeleap(clock, 1, DAYS)) // + .next(new TestCase("25") // + .output(STATE_MACHINE, State.ACTIVATION_TIME)) + .next(new TestCase("26") // + .timeleap(clock, 4, HOURS)) // + .next(new TestCase("27") // + .output(STATE_MACHINE, State.ACTIVATION_TIME)) + .next(new TestCase("28")// + .input("meter0", FREQUENCY, 49400)) + .next(new TestCase("29") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("30") // + .output(STATE_MACHINE, State.SUPPORT_DURATION)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 92000)) // + .next(new TestCase("31") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("32") // + .output(STATE_MACHINE, State.SUPPORT_DURATION)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 92000)) // + .next(new TestCase("33") // + .timeleap(clock, LONG_SUPPORT_DURATION.getValue(), SECONDS) // + .output(STATE_MACHINE, State.SUPPORT_DURATION)) + .next(new TestCase("34") // + .output(STATE_MACHINE, State.DEACTIVATION_TIME).output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("35") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("36") // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("37") // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY)) + .next(new TestCase("38") // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY)) + .next(new TestCase("39") // + .timeleap(clock, LONG_SUPPORT_DURATION.getValue(), MINUTES)) + .next(new TestCase("40") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY)) + .next(new TestCase("41") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.RECOVERY_TIME)) + .next(new TestCase("42") // + .timeleap(clock, 1, DAYS)) // + .deactivate(); + } + + @Test + public void testInvalidJsonSchedule() throws Exception { + final var clock = createDummyClock(); + new ControllerTest(new ControllerFastFrequencyReserveImpl()) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedSymmetricEss("ess0") // + .withGridMode(GridMode.ON_GRID)// + .withMaxApparentPower(92000)// + .withAllowedChargePower(-92000)// + .withAllowedDischargePower(92000)) // + .addReference("meter", new DummyElectricityMeter("meter0")) // + .activate(MyConfig.create() // + .setEssId("ess0") // + .setId("ctrl0") // + .setMeterId("meter0") // + .setMode(ControlMode.MANUAL_ON) // + .setPreActivationTime(15)// + .setactivationScheduleJson("foo")// + .build()) // + .deactivate(); + } + + @Test + public void testInvalidJsonSchedule1() throws Exception { + final var clock = createDummyClock(); + new ControllerTest(new ControllerFastFrequencyReserveImpl()) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedSymmetricEss("ess0") // + .withGridMode(GridMode.ON_GRID)// + .withMaxApparentPower(92000)// + .withAllowedChargePower(-92000)// + .withAllowedDischargePower(92000)) // + .addReference("meter", new DummyElectricityMeter("meter0")) // + .activate(MyConfig.create() // + .setEssId("ess0") // + .setId("ctrl0") // + .setMeterId("meter0") // + .setMode(ControlMode.MANUAL_ON) // + .setPreActivationTime(15)// + .setactivationScheduleJson("[foo]")// + .build()) // + .deactivate(); + } + + public static final String MY_JSON = """ + [ + { + "startTimestamp":"1701738000", + "duration":"10800", + "dischargePowerSetPoint":"92000", + "frequencyLimit":"50000", + "activationRunTime":"LONG_ACTIVATION_RUN", + "supportDuration":"LONG_SUPPORT_DURATION" + }, + { + "startTimestamp":"1701738000", + "duration":"10800", + "dischargePowerSetPoint":"92000", + "frequencyLimit":"50000", + "activationRunTime":"LONG_ACTIVATION_RUN", + "supportDuration":"LONG_SUPPORT_DURATION" + }, + { + "startTimestamp":"1701752400", + "duration":"10800", + "dischargePowerSetPoint":"92000", + "frequencyLimit":"50000", + "activationRunTime":"LONG_ACTIVATION_RUN", + "supportDuration":"LONG_SUPPORT_DURATION" + }, + { + "startTimestamp":"1701777600", + "duration":"10800", + "dischargePowerSetPoint":"92000", + "frequencyLimit":"50000", + "activationRunTime":"LONG_ACTIVATION_RUN", + "supportDuration":"LONG_SUPPORT_DURATION" + } + ] + """; + + @Test + public void testFromMethod() throws OpenemsNamedException { + var scheduleArray = JsonUtils.parseToJsonArray(MY_JSON); + try { + List scheduleList = ActivateFastFreqReserveSchedule.from(scheduleArray); + + assertEquals(3, scheduleList.size()); + assertTrue(scheduleList.get(0).startTimestamp() <= scheduleList.get(1).startTimestamp()); + assertTrue(scheduleList.get(1).startTimestamp() <= scheduleList.get(2).startTimestamp()); + + } catch (OpenemsNamedException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserveImplTest2.java b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserveImplTest2.java new file mode 100644 index 00000000000..d1bbb7c9da1 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/ControllerFastFrequencyReserveImplTest2.java @@ -0,0 +1,182 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve; + +import static io.openems.common.utils.JsonUtils.buildJsonArray; +import static io.openems.common.utils.JsonUtils.buildJsonObject; +import static io.openems.edge.controller.ess.fastfrequencyreserve.ControllerFastFrequencyReserve.ChannelId.STATE_MACHINE; +import static io.openems.edge.controller.ess.fastfrequencyreserve.enums.ActivationTime.LONG_ACTIVATION_RUN; +import static io.openems.edge.controller.ess.fastfrequencyreserve.enums.SupportDuration.LONG_SUPPORT_DURATION; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.FREQUENCY; +import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.SECONDS; + +import java.time.Instant; +import java.time.ZoneOffset; + +import org.junit.Test; + +import io.openems.common.test.TimeLeapClock; +import io.openems.edge.common.sum.GridMode; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ControlMode; +import io.openems.edge.controller.ess.fastfrequencyreserve.statemachine.StateMachine.State; +import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.meter.test.DummyElectricityMeter; + +public class ControllerFastFrequencyReserveImplTest2 { + + @Test + public void test1() throws Exception { + final var clock = new TimeLeapClock(Instant.parse("2023-07-13T08:45:00.00Z"), ZoneOffset.UTC); + new ControllerTest(new ControllerFastFrequencyReserveImpl()) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedSymmetricEss("ess0") // + .withGridMode(GridMode.ON_GRID)// + .withMaxApparentPower(92000)// + .withAllowedChargePower(-92000)// + .withAllowedDischargePower(92000)) // + .addReference("meter", new DummyElectricityMeter("meter0")) // + .activate(MyConfig.create() // + .setEssId("ess0") // + .setId("ctrl0") // + .setMeterId("meter0") // + .setMode(ControlMode.MANUAL_ON) // + .setPreActivationTime(15)// + .setactivationScheduleJson(buildJsonArray() // + .add(buildJsonObject() // + .addProperty("startTimestamp", 1689242400) // Thu Jul 13 2023 10:00:00 GMT+0000 + .addProperty("duration", 86400) // + .addProperty("dischargePowerSetPoint", 92000) // + .addProperty("frequencyLimit", 49500) // + .addProperty("activationRunTime", LONG_ACTIVATION_RUN) // + .addProperty("supportDuration", LONG_SUPPORT_DURATION) // + .build()) + .add(buildJsonObject() // + .addProperty("startTimestamp", 1689331500) // Fri Jul 14 2023 10:45:00 GMT+0000 + .addProperty("duration", 86400) // + .addProperty("dischargePowerSetPoint", 92000) // + .addProperty("frequencyLimit", 49500) // + .addProperty("activationRunTime", LONG_ACTIVATION_RUN) // + .addProperty("supportDuration", LONG_SUPPORT_DURATION) // + .build()) + .build()// + .toString())// + .build()) + .next(new TestCase("1") // + .input("meter0", FREQUENCY, 50000)// + .output(ControllerFastFrequencyReserve.ChannelId.STATE_MACHINE, State.UNDEFINED) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, null))// + .next(new TestCase("2") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.UNDEFINED) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, null))// + .next(new TestCase("3") // + .timeleap(clock, 1, HOURS) // + .input("meter0", FREQUENCY, 50000))// + .next(new TestCase("4"))// + .next(new TestCase("5") // + .output(STATE_MACHINE, State.UNDEFINED)) + .next(new TestCase("6") // + .timeleap(clock, 10, MINUTES)// + .output(STATE_MACHINE, State.PRE_ACTIVATION_STATE)) + .next(new TestCase("7") // + .timeleap(clock, 10, MINUTES)) + .next(new TestCase("8") // + .output(STATE_MACHINE, State.ACTIVATION_TIME)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("9") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.ACTIVATION_TIME)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("10") // + .input("meter0", FREQUENCY, 49400)// + .output(STATE_MACHINE, State.ACTIVATION_TIME)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 92000)) + .next(new TestCase("11") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("12") // + .output(STATE_MACHINE, State.SUPPORT_DURATION)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 92000)) // + .next(new TestCase("13") // + .timeleap(clock, LONG_SUPPORT_DURATION.getValue(), SECONDS) // + .output(STATE_MACHINE, State.SUPPORT_DURATION)) + .next(new TestCase("14") // + .output(STATE_MACHINE, State.DEACTIVATION_TIME).output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("15") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("16") // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("17") // + .timeleap(clock, 16, SECONDS) // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY)) + .next(new TestCase("18") // + .timeleap(clock, 12, MINUTES)) // + .next(new TestCase("19") // + .output(STATE_MACHINE, State.RECOVERY_TIME) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -16560)) + .next(new TestCase("20") // + .output(STATE_MACHINE, State.RECOVERY_TIME)) + .next(new TestCase("21") // + .output(STATE_MACHINE, State.RECOVERY_TIME)) + .next(new TestCase("22") // + .timeleap(clock, LONG_SUPPORT_DURATION.getValue(), MINUTES)) + .next(new TestCase("23") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.ACTIVATION_TIME)) + .next(new TestCase("24") // + .timeleap(clock, 1, DAYS)) // + .next(new TestCase("25") // + .output(STATE_MACHINE, State.ACTIVATION_TIME)) + .next(new TestCase("26") // + .timeleap(clock, 4, HOURS)) // + .next(new TestCase("27") // + .output(STATE_MACHINE, State.ACTIVATION_TIME)) + .next(new TestCase("28")// + .input("meter0", FREQUENCY, 49400)) + .next(new TestCase("29") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("30") // + .output(STATE_MACHINE, State.SUPPORT_DURATION)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 92000)) // + .next(new TestCase("31") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("32") // + .output(STATE_MACHINE, State.SUPPORT_DURATION)// + .output("ess0", SET_ACTIVE_POWER_EQUALS, 92000)) // + .next(new TestCase("33") // + .timeleap(clock, LONG_SUPPORT_DURATION.getValue(), SECONDS) // + .output(STATE_MACHINE, State.SUPPORT_DURATION)) + .next(new TestCase("34") // + .output(STATE_MACHINE, State.DEACTIVATION_TIME) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("35") // + .timeleap(clock, LONG_ACTIVATION_RUN.getValue(), MILLIS)) // + .next(new TestCase("36") // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) + .next(new TestCase("37") // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY)) + .next(new TestCase("38") // + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY)) + .next(new TestCase("39") // + .timeleap(clock, LONG_SUPPORT_DURATION.getValue(), MINUTES)) + .next(new TestCase("40") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.BUFFERED_TIME_BEFORE_RECOVERY)) + .next(new TestCase("41") // + .input("meter0", FREQUENCY, 50000)// + .output(STATE_MACHINE, State.RECOVERY_TIME)) + .next(new TestCase("42") // + .timeleap(clock, 1, DAYS)) // + .deactivate(); + } + +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/JsonRpcTest.java b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/JsonRpcTest.java new file mode 100644 index 00000000000..c0db3e9a28d --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/JsonRpcTest.java @@ -0,0 +1,68 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve; + +import java.net.URI; +//import org.junit.Test; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ActivationTime; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.SupportDuration; +import io.openems.edge.controller.ess.fastfrequencyreserve.jsonrpc.SetActivateFastFreqReserveRequest; +import io.openems.edge.controller.ess.fastfrequencyreserve.jsonrpc.SetActivateFastFreqReserveRequest.ActivateFastFreqReserveSchedule; + +/** + * This Test demonstrates the usage of the OpenEMS Backend-to-Backend API + * interface. To start the tests make sure to start OpenEMS Backend and activate + * the B2bWebsocket component via Apache Felix. Afterwards uncomment the "@Test" + * annotations below and execute the Tests. + */ +public class JsonRpcTest { + + private static final String URI = "ws://localhost:8076"; + private static final String USERNAME = "user"; + private static final String PASSWORD = "password"; + + private static TestClient prepareTestClient() throws URISyntaxException, InterruptedException { + Map httpHeaders = new HashMap<>(); + var auth = new String( + Base64.getEncoder().encode((JsonRpcTest.USERNAME + ":" + JsonRpcTest.PASSWORD).getBytes()), + StandardCharsets.UTF_8); + httpHeaders.put("Authorization", "Basic " + auth); + var client = new TestClient(new URI(JsonRpcTest.URI), httpHeaders); + client.startBlocking(); + return client; + } + + /** + * Tests the activation of Fast Frequency Reserve schedule. + * + * @throws URISyntaxException String could not be parsed as a URI reference. + * @throws InterruptedException interrupted exception. + */ + // @Test + public void testActivateFastFreqReserveSchedule() throws URISyntaxException, InterruptedException { + var client = JsonRpcTest.prepareTestClient(); + + var request = new SetActivateFastFreqReserveRequest("edge0"); + var now = System.currentTimeMillis() / 1000; + ActivateFastFreqReserveSchedule newEntry = new ActivateFastFreqReserveSchedule(// + now, // + 1000, // + 92000, // + 50000, // + ActivationTime.LONG_ACTIVATION_RUN, // + SupportDuration.LONG_SUPPORT_DURATION); + request.addScheduleEntry(newEntry); + + try { + var responseFuture = client.sendRequest(request); + System.out.println(responseFuture.get().toString()); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/MyConfig.java b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/MyConfig.java new file mode 100644 index 00000000000..948d3e0fb3a --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/MyConfig.java @@ -0,0 +1,105 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.utils.ConfigUtils; +import io.openems.edge.controller.ess.fastfrequencyreserve.enums.ControlMode; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String essId; + private String meterId; + private ControlMode mode; + private String activationScheduleJson; + private int preActivationTime; + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setEssId(String essId) { + this.essId = essId; + return this; + } + + public Builder setMeterId(String meterId) { + this.meterId = meterId; + return this; + } + + public Builder setMode(ControlMode mode) { + this.mode = mode; + return this; + } + + public Builder setactivationScheduleJson(String schedule) { + this.activationScheduleJson = schedule; + return this; + } + + public Builder setPreActivationTime(int preActivationTime) { + this.preActivationTime = preActivationTime; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String ess_id() { + return this.builder.essId; + } + + @Override + public String meter_id() { + return this.builder.meterId; + } + + @Override + public String ess_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.ess_id()); + } + + @Override + public String meter_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.meter_id()); + } + + @Override + public ControlMode controlMode() { + return this.builder.mode; + } + + @Override + public String activationScheduleJson() { + return this.builder.activationScheduleJson; + } + + @Override + public int preActivationTime() { + return this.builder.preActivationTime; + } + +} diff --git a/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/TestClient.java b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/TestClient.java new file mode 100644 index 00000000000..991887efda7 --- /dev/null +++ b/io.openems.edge.controller.ess.fastfrequencyreserve/test/io/openems/edge/controller/ess/fastfrequencyreserve/TestClient.java @@ -0,0 +1,122 @@ +package io.openems.edge.controller.ess.fastfrequencyreserve; + +import java.net.URI; +import java.util.Map; + +import org.java_websocket.WebSocket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.common.websocket.AbstractWebsocketClient; +import io.openems.common.websocket.OnClose; +import io.openems.common.websocket.OnError; +import io.openems.common.websocket.OnNotification; +import io.openems.common.websocket.OnOpen; +import io.openems.common.websocket.OnRequest; +import io.openems.common.websocket.WsData; + +public class TestClient extends AbstractWebsocketClient { + + private final Logger log = LoggerFactory.getLogger(TestClient.class); + + private OnOpen onOpen; + private OnRequest onRequest; + private OnNotification onNotification; + private OnError onError; + private OnClose onClose; + + protected TestClient(URI serverUri, Map httpHeaders) { + super("JsonTest.Unittest", serverUri, httpHeaders); + this.onOpen = (ws, handshake) -> { + return null; + }; + this.onRequest = (ws, request) -> { + this.log.info("OnRequest: " + request); + return null; + }; + this.onNotification = (ws, notification) -> { + this.log.info("OnNotification: " + notification); + }; + this.onError = (ws, ex) -> { + this.log.info("onError: " + ex.getMessage()); + }; + this.onClose = (ws, code, reason, remote) -> { + this.log.info("onClose: " + reason); + }; + } + + @Override + public OnOpen getOnOpen() { + return this.onOpen; + } + + public void setOnOpen(OnOpen onOpen) { + this.onOpen = onOpen; + } + + @Override + public OnRequest getOnRequest() { + return this.onRequest; + } + + public void setOnRequest(OnRequest onRequest) { + this.onRequest = onRequest; + } + + @Override + public OnError getOnError() { + return this.onError; + } + + public void setOnError(OnError onError) { + this.onError = onError; + } + + @Override + public OnClose getOnClose() { + return this.onClose; + } + + public void setOnClose(OnClose onClose) { + this.onClose = onClose; + } + + @Override + protected OnNotification getOnNotification() { + return this.onNotification; + } + + public void setOnNotification(OnNotification onNotification) { + this.onNotification = onNotification; + } + + @Override + protected WsData createWsData(WebSocket ws) { + return new WsData(ws) { + @Override + public String toString() { + return "TestClient.WsData []"; + } + }; + } + + @Override + protected void logInfo(Logger log, String message) { + log.info(message); + } + + @Override + protected void logWarn(Logger log, String message) { + log.warn(message); + } + + @Override + protected void logError(Logger log, String message) { + log.error(message); + } + + @Override + protected void execute(Runnable command) { + command.run(); + } +} diff --git a/io.openems.edge.controller.ess.fixactivepower/bnd.bnd b/io.openems.edge.controller.ess.fixactivepower/bnd.bnd index 9b0e7d57001..38c5412d005 100644 --- a/io.openems.edge.controller.ess.fixactivepower/bnd.bnd +++ b/io.openems.edge.controller.ess.fixactivepower/bnd.bnd @@ -8,8 +8,9 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.common,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ + io.openems.edge.energy.api,\ io.openems.edge.ess.api,\ io.openems.edge.timedata.api,\ - + -testpath: \ ${testpath} diff --git a/io.openems.edge.controller.ess.fixactivepower/src/io/openems/edge/controller/ess/fixactivepower/ControllerEssFixActivePowerImpl.java b/io.openems.edge.controller.ess.fixactivepower/src/io/openems/edge/controller/ess/fixactivepower/ControllerEssFixActivePowerImpl.java index ad3f5905e0e..2fe3a64a3e7 100644 --- a/io.openems.edge.controller.ess.fixactivepower/src/io/openems/edge/controller/ess/fixactivepower/ControllerEssFixActivePowerImpl.java +++ b/io.openems.edge.controller.ess.fixactivepower/src/io/openems/edge/controller/ess/fixactivepower/ControllerEssFixActivePowerImpl.java @@ -1,5 +1,9 @@ package io.openems.edge.controller.ess.fixactivepower; +import static io.openems.edge.energy.api.EnergyUtils.toEnergy; + +import java.util.function.Supplier; + import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -17,10 +21,13 @@ import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.controller.api.Controller; +import io.openems.edge.energy.api.EnergySchedulable; +import io.openems.edge.energy.api.EnergyScheduleHandler; import io.openems.edge.ess.api.HybridEss; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.ess.api.PowerConstraint; import io.openems.edge.ess.power.api.Pwr; +import io.openems.edge.ess.power.api.Relationship; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateActiveTime; @@ -32,10 +39,11 @@ configurationPolicy = ConfigurationPolicy.REQUIRE // ) public class ControllerEssFixActivePowerImpl extends AbstractOpenemsComponent - implements ControllerEssFixActivePower, Controller, OpenemsComponent, TimedataProvider { + implements ControllerEssFixActivePower, EnergySchedulable, Controller, OpenemsComponent, TimedataProvider { private final CalculateActiveTime calculateCumulatedActiveTime = new CalculateActiveTime(this, ControllerEssFixActivePower.ChannelId.CUMULATED_ACTIVE_TIME); + private final EnergyScheduleHandler energyScheduleHandler; @Reference private ConfigurationAdmin cm; @@ -54,6 +62,13 @@ public ControllerEssFixActivePowerImpl() { Controller.ChannelId.values(), // ControllerEssFixActivePower.ChannelId.values() // ); + this.energyScheduleHandler = buildEnergyScheduleHandler(() -> new EshContext(// + this.config.mode(), // + toEnergy(switch (this.config.phase()) { + case ALL -> this.config.power(); + case L1, L2, L3 -> this.config.power() * 3; + }), // + this.config.relationship())); } @Activate @@ -70,6 +85,7 @@ private void modified(ComponentContext context, Config config) { if (this.applyConfig(context, config)) { return; } + this.energyScheduleHandler.triggerReschedule(); } private boolean applyConfig(ComponentContext context, Config config) { @@ -137,4 +153,40 @@ protected static Integer getAcPower(ManagedSymmetricEss ess, HybridEssMode hybri public Timedata getTimedata() { return this.timedata; } + + /** + * Builds the {@link EnergyScheduleHandler}. + * + *

+ * This is public so that it can be used by the EnergyScheduler integration + * test. + * + * @param context a supplier for the configured {@link EshContext} + * @return a {@link EnergyScheduleHandler} + */ + public static EnergyScheduleHandler buildEnergyScheduleHandler(Supplier context) { + return EnergyScheduleHandler.of(// + simContext -> context.get(), // + (simContext, period, energyFlow, ctrlContext) -> { + switch (ctrlContext.mode) { + case MANUAL_ON: + switch (ctrlContext.relationship) { + case EQUALS -> energyFlow.setEss(ctrlContext.energy); + case GREATER_OR_EQUALS -> energyFlow.setEssMaxCharge(-ctrlContext.energy); + case LESS_OR_EQUALS -> energyFlow.setEssMaxDischarge(ctrlContext.energy); + } + break; + case MANUAL_OFF: + break; + } + }); + } + + public static record EshContext(Mode mode, int energy, Relationship relationship) { + } + + @Override + public EnergyScheduleHandler getEnergyScheduleHandler() { + return this.energyScheduleHandler; + } } \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fixactivepower/src/io/openems/edge/controller/ess/fixactivepower/package-info.java b/io.openems.edge.controller.ess.fixactivepower/src/io/openems/edge/controller/ess/fixactivepower/package-info.java new file mode 100644 index 00000000000..aab27855be8 --- /dev/null +++ b/io.openems.edge.controller.ess.fixactivepower/src/io/openems/edge/controller/ess/fixactivepower/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.controller.ess.fixactivepower; diff --git a/io.openems.edge.controller.ess.fixactivepower/test/io/openems/edge/controller/ess/fixactivepower/ControllerEssFixActivePowerImplTest.java b/io.openems.edge.controller.ess.fixactivepower/test/io/openems/edge/controller/ess/fixactivepower/ControllerEssFixActivePowerImplTest.java index d26fa505e44..ebcde754b1d 100644 --- a/io.openems.edge.controller.ess.fixactivepower/test/io/openems/edge/controller/ess/fixactivepower/ControllerEssFixActivePowerImplTest.java +++ b/io.openems.edge.controller.ess.fixactivepower/test/io/openems/edge/controller/ess/fixactivepower/ControllerEssFixActivePowerImplTest.java @@ -1,5 +1,6 @@ package io.openems.edge.controller.ess.fixactivepower; +import static io.openems.edge.controller.ess.fixactivepower.ControllerEssFixActivePowerImpl.getAcPower; import static org.junit.Assert.assertEquals; import org.junit.Test; @@ -14,45 +15,44 @@ public class ControllerEssFixActivePowerImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - @Test public void testOn() throws OpenemsException, Exception { - final var ess = new DummyManagedAsymmetricEss(ESS_ID); + final var ess = new DummyManagedAsymmetricEss("ess0"); new ControllerTest(new ControllerEssFixActivePowerImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("ess", ess) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setMode(Mode.MANUAL_ON) // .setHybridEssMode(HybridEssMode.TARGET_DC) // .setPower(1234) // .setPhase(Phase.ALL) // .setRelationship(Relationship.EQUALS) // - .build()); // + .build()) // + .deactivate(); } @Test public void testOff() throws OpenemsException, Exception { new ControllerTest(new ControllerEssFixActivePowerImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedAsymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedAsymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setMode(Mode.MANUAL_OFF) // .setHybridEssMode(HybridEssMode.TARGET_DC) // .setPower(1234) // .setPhase(Phase.ALL) // .setRelationship(Relationship.EQUALS) // - .build()); // + .build()) // + .deactivate(); } @Test public void testGetAcPower() throws OpenemsException, Exception { - var hybridEss = new DummyHybridEss(ESS_ID) // + var hybridEss = new DummyHybridEss("ess0") // .withActivePower(7000) // .withMaxApparentPower(10000) // .withAllowedChargePower(-5000) // @@ -60,9 +60,9 @@ public void testGetAcPower() throws OpenemsException, Exception { .withDcDischargePower(3000); // assertEquals(Integer.valueOf(5000), // - ControllerEssFixActivePowerImpl.getAcPower(hybridEss, HybridEssMode.TARGET_AC, 5000)); + getAcPower(hybridEss, HybridEssMode.TARGET_AC, 5000)); assertEquals(Integer.valueOf(9000), // - ControllerEssFixActivePowerImpl.getAcPower(hybridEss, HybridEssMode.TARGET_DC, 5000)); + getAcPower(hybridEss, HybridEssMode.TARGET_DC, 5000)); } } diff --git a/io.openems.edge.controller.ess.fixstateofcharge/test/io/openems/edge/controller/ess/fixstateofcharge/ControllerEssFixStateOfChargeImplTest.java b/io.openems.edge.controller.ess.fixstateofcharge/test/io/openems/edge/controller/ess/fixstateofcharge/ControllerEssFixStateOfChargeImplTest.java index 48553f06491..70d0b636428 100644 --- a/io.openems.edge.controller.ess.fixstateofcharge/test/io/openems/edge/controller/ess/fixstateofcharge/ControllerEssFixStateOfChargeImplTest.java +++ b/io.openems.edge.controller.ess.fixstateofcharge/test/io/openems/edge/controller/ess/fixstateofcharge/ControllerEssFixStateOfChargeImplTest.java @@ -1,10 +1,23 @@ package io.openems.edge.controller.ess.fixstateofcharge; +import static io.openems.edge.controller.ess.fixstateofcharge.api.AbstractFixStateOfCharge.DEFAULT_POWER_FACTOR; +import static io.openems.edge.controller.ess.fixstateofcharge.api.EndCondition.CAPACITY_CHANGED; +import static io.openems.edge.controller.ess.fixstateofcharge.api.FixStateOfCharge.ChannelId.DEBUG_SET_ACTIVE_POWER; +import static io.openems.edge.controller.ess.fixstateofcharge.api.FixStateOfCharge.ChannelId.DEBUG_SET_ACTIVE_POWER_RAW; +import static io.openems.edge.controller.ess.fixstateofcharge.api.FixStateOfCharge.ChannelId.STATE_MACHINE; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.CAPACITY; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.MAX_APPARENT_POWER; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.SOC; +import static java.lang.Math.min; +import static java.lang.Math.round; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MINUTES; + import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; import org.junit.Test; @@ -14,54 +27,30 @@ import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.controller.ess.fixstateofcharge.api.AbstractFixStateOfCharge; -import io.openems.edge.controller.ess.fixstateofcharge.api.EndCondition; -import io.openems.edge.controller.ess.fixstateofcharge.statemachine.StateMachine; +import io.openems.edge.controller.ess.fixstateofcharge.statemachine.StateMachine.State; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.timedata.test.DummyTimedata; public class ControllerEssFixStateOfChargeImplTest { - // Ids - private static final String CTRL_ID = "ctrlFixStateOfCharge0"; - private static final String ESS_ID = "ess0"; - - // Components - private static final DummyManagedSymmetricEss ESS = new DummyManagedSymmetricEss(ESS_ID) // + private static final DummyManagedSymmetricEss ESS = new DummyManagedSymmetricEss("ess0") // .withMaxApparentPower(10_000); - - // Defaults private static final String DEFAULT_TARGET_TIME = "2022-10-27T10:30:00+01:00"; - - // Ess channels - private static final ChannelAddress ESS_CAPACITY = new ChannelAddress(ESS_ID, "Capacity"); - private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); - private static final ChannelAddress ESS_MAX_APPARENT_POWER = new ChannelAddress(ESS_ID, "MaxApparentPower"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - - // Controller channels - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(CTRL_ID, "StateMachine"); - private static final ChannelAddress DEBUG_SET_ACTIVE_POWER = new ChannelAddress(CTRL_ID, "DebugSetActivePower"); - private static final ChannelAddress DEBUG_SET_ACTIVE_POWER_RAW = new ChannelAddress(CTRL_ID, - "DebugSetActivePowerRaw"); - private static final ChannelAddress CTRL_ESS_CAPACITY = new ChannelAddress(CTRL_ID, "EssCapacity"); + private static final ChannelAddress CTRL_ESS_CAPACITY = new ChannelAddress("ctrl0", "EssCapacity"); @Test public void testNotRunning() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2022-01-01T08:00:00.00Z"), ZoneOffset.UTC); - final var componentManager = new DummyComponentManager(clock); - new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("componentManager", componentManager) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("sum", new DummySum()) // .addReference("ess", ESS) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(false) // .setTargetSoc(30) // .setSpecifyTargetTime(true) // @@ -69,29 +58,30 @@ public void testNotRunning() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(false) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase()) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, null) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, null) // .output(DEBUG_SET_ACTIVE_POWER, null) // .output(DEBUG_SET_ACTIVE_POWER_RAW, null) // - .output(STATE_MACHINE, StateMachine.State.IDLE) // - ); + .output(STATE_MACHINE, State.IDLE)) // + .deactivate(); } @Test public void testAllStates() throws Exception { + final var clock = new TimeLeapClock(Instant.parse("2023-01-01T08:00:00.00Z"), ZoneOffset.UTC); new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // - .addReference("componentManager", new DummyComponentManager()) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(true) // @@ -99,39 +89,39 @@ public void testAllStates() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(true) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 21) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 21) // + .output(STATE_MACHINE, State.NOT_STARTED)) // .next(new TestCase() // - .input(ESS_SOC, 20) // - .output(STATE_MACHINE, StateMachine.State.BELOW_TARGET_SOC)) // + .input("ess0", SOC, 20) // + .output(STATE_MACHINE, State.BELOW_TARGET_SOC)) // .next(new TestCase() // - .input(ESS_SOC, 25) // - .output(STATE_MACHINE, StateMachine.State.BELOW_TARGET_SOC)) // + .input("ess0", SOC, 25) // + .output(STATE_MACHINE, State.BELOW_TARGET_SOC)) // .next(new TestCase() // - .input(ESS_SOC, 30)) // + .input("ess0", SOC, 30)) // .next(new TestCase() // - .input(ESS_SOC, 30) /// - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC)) // + .input("ess0", SOC, 30) /// + .output(STATE_MACHINE, State.AT_TARGET_SOC)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC)) // - ; + .input("ess0", SOC, 30) // + .output(STATE_MACHINE, State.AT_TARGET_SOC)) // + .deactivate(); } @Test public void testCapacityCondition() throws Exception { - + final var clock = new TimeLeapClock(Instant.parse("2023-01-01T08:00:00.00Z"), ZoneOffset.UTC); var timedata = new DummyTimedata("timedata0"); var start = ZonedDateTime.of(2022, 05, 05, 0, 0, 0, 0, ZoneId.of("UTC")); @@ -139,15 +129,15 @@ public void testCapacityCondition() throws Exception { timedata.add(start.plusMinutes(60), CTRL_ESS_CAPACITY, 8_000); timedata.add(start.plusMinutes(90), CTRL_ESS_CAPACITY, 8_000); - var test = new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // - .addReference("componentManager", new DummyComponentManager()) // + new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // .addReference("timedata", timedata) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(true) // @@ -155,70 +145,66 @@ public void testCapacityCondition() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(true) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 22) // - .input(ESS_MAX_APPARENT_POWER, 10000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 22) // + .input("ess0", MAX_APPARENT_POWER, 10000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 21) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 21) // + .output(STATE_MACHINE, State.NOT_STARTED)) // .next(new TestCase() // - .input(ESS_SOC, 20) // - .output(STATE_MACHINE, StateMachine.State.BELOW_TARGET_SOC)) // + .input("ess0", SOC, 20) // + .output(STATE_MACHINE, State.BELOW_TARGET_SOC)) // .next(new TestCase() // - .input(ESS_SOC, 25) // + .input("ess0", SOC, 25) // .input(CTRL_ESS_CAPACITY, 8_000) // - .input(ESS_CAPACITY, 8_000) // + .input("ess0", CAPACITY, 8_000) // .output(CTRL_ESS_CAPACITY, 8_000) // - .output(STATE_MACHINE, StateMachine.State.BELOW_TARGET_SOC)) // + .output(STATE_MACHINE, State.BELOW_TARGET_SOC)) // .next(new TestCase() // - .input(ESS_CAPACITY, 8_000) // - .input(ESS_SOC, 30) // + .input("ess0", CAPACITY, 8_000) // + .input("ess0", SOC, 30) // .output(CTRL_ESS_CAPACITY, 8_000)) // .next(new TestCase() // - .input(ESS_SOC, 30) /// - .input(ESS_CAPACITY, 8_000) // + .input("ess0", SOC, 30) /// + .input("ess0", CAPACITY, 8_000) // .output(CTRL_ESS_CAPACITY, 8_000) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC)) // - ; + .output(STATE_MACHINE, State.AT_TARGET_SOC)) // - // EMS restart - test.next(new TestCase() // - .input(ESS_CAPACITY, null) // - .input(CTRL_ESS_CAPACITY, null) // - .output(CTRL_ESS_CAPACITY, null)); // + .next(new TestCase("EMS restart") // + .input("ess0", CAPACITY, null) // + .input(CTRL_ESS_CAPACITY, null) // + .output(CTRL_ESS_CAPACITY, null)) // - // New Ess.Capacity (Ctrl is taking the last one from timedata) - test.next(new TestCase() // - .input(ESS_CAPACITY, 10_000) // - .input(CTRL_ESS_CAPACITY, null) // - .output(CTRL_ESS_CAPACITY, 8_000)); // + .next(new TestCase("New Ess.Capacity (Ctrl is taking the last one from timedata)") // + .input("ess0", CAPACITY, 10_000) // + .input(CTRL_ESS_CAPACITY, null) // + .output(CTRL_ESS_CAPACITY, 8_000)) // - test.next(new TestCase() // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // - ; + .next(new TestCase() // + .output(STATE_MACHINE, State.IDLE)) // + + .deactivate(); } @Test public void testAboveLimit() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2022-01-01T08:00:00.00Z"), ZoneOffset.UTC); - final var componentManager = new DummyComponentManager(clock); - new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("componentManager", componentManager) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("ess", ESS) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(false) // @@ -226,44 +212,42 @@ public void testAboveLimit() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(false) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 50) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 50) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 50) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 50) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 50) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 50) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.NOT_STARTED)) // .next(new TestCase() // - .input(ESS_SOC, 50) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.ABOVE_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 500) // + .input("ess0", SOC, 50) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.ABOVE_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 500) // .output(DEBUG_SET_ACTIVE_POWER_RAW, 500) // - .output(DEBUG_SET_ACTIVE_POWER, 500) // Would increase till 10_000 - ); + .output(DEBUG_SET_ACTIVE_POWER, 500)) // Would increase till 10_000 + .deactivate(); } @Test public void testBelowLimit() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2022-01-01T08:00:00.00Z"), ZoneOffset.UTC); - final var componentManager = new DummyComponentManager(clock); - new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("componentManager", componentManager) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("ess", ESS) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(false) // @@ -271,44 +255,42 @@ public void testBelowLimit() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(false) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.NOT_STARTED)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.BELOW_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -500) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.BELOW_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -500) // .output(DEBUG_SET_ACTIVE_POWER_RAW, -500) // - .output(DEBUG_SET_ACTIVE_POWER, -500) // Would increase till 10_000 - ); + .output(DEBUG_SET_ACTIVE_POWER, -500)) // Would increase till 10_000 + .deactivate(); } @Test public void testAtLimit() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2022-10-27T08:00:00.00Z"), ZoneOffset.UTC); - final var componentManager = new DummyComponentManager(clock); - new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("componentManager", componentManager) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("ess", ESS) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(false) // @@ -316,85 +298,85 @@ public void testAtLimit() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(false) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.NOT_STARTED)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.BELOW_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -500) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.BELOW_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -500) // .output(DEBUG_SET_ACTIVE_POWER_RAW, -500) // .output(DEBUG_SET_ACTIVE_POWER, -500)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1000)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1000)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1500)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1500)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -2000)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -2000)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -2500)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -2500)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -3000)) // + .input("ess0", SOC, 10) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -3000)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -3500)) // + .input("ess0", SOC, 10) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -3500)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -4000)) // + .input("ess0", SOC, 10) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -4000)) // .next(new TestCase()) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -5000)) // + .input("ess0", SOC, 10) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -5000)) // .next(new TestCase()) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -6000)) // + .input("ess0", SOC, 10) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -6000)) // .next(new TestCase()) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -7000)) // + .input("ess0", SOC, 10) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -7000)) // .next(new TestCase()) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -8000)) // + .input("ess0", SOC, 10) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -8000)) // .next(new TestCase()) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -9000)) // + .input("ess0", SOC, 10) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -9000)) // .next(new TestCase()) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -10000)) // + .input("ess0", SOC, 10) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -10000)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .input(ESS_MAX_APPARENT_POWER, 10_000)) // + .input("ess0", SOC, 30) // + .input("ess0", MAX_APPARENT_POWER, 10_000)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -9000)) // + .input("ess0", SOC, 30) // + .output(STATE_MACHINE, State.AT_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -9000)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -8000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -8000)) // .next(new TestCase()) // .next(new TestCase()) // .next(new TestCase()) // @@ -402,26 +384,24 @@ public void testAtLimit() throws Exception { .next(new TestCase()) // .next(new TestCase()) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1000)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // - ; + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // + .deactivate(); } @Test public void testAtLimitDeadBand() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2022-10-27T08:00:00.00Z"), ZoneOffset.UTC); - final var componentManager = new DummyComponentManager(clock); - new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("componentManager", componentManager) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("ess", ESS) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(false) // @@ -429,78 +409,76 @@ public void testAtLimitDeadBand() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(false) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 30) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .input(ESS_CAPACITY, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 30) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .input("ess0", CAPACITY, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 30) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 30) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.NOT_STARTED)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0) // + .input("ess0", SOC, 30) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.AT_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0) // .output(DEBUG_SET_ACTIVE_POWER_RAW, 0) // .output(DEBUG_SET_ACTIVE_POWER, 0)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // + .input("ess0", SOC, 30) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // .next(new TestCase() // - .input(ESS_SOC, 31) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // + .input("ess0", SOC, 31) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // .next(new TestCase() // - .input(ESS_SOC, 29) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // + .input("ess0", SOC, 29) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // .next(new TestCase() // - .input(ESS_SOC, 28)) // + .input("ess0", SOC, 28)) // .next(new TestCase() // - .input(ESS_SOC, 28) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -500)) // + .input("ess0", SOC, 28) // + .output(STATE_MACHINE, State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -500)) // .next(new TestCase() // - .input(ESS_SOC, 28) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1000)) // + .input("ess0", SOC, 28) // + .output(STATE_MACHINE, State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1000)) // .next(new TestCase() // - .input(ESS_SOC, 28) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1500)) // + .input("ess0", SOC, 28) // + .output(STATE_MACHINE, State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1500)) // .next(new TestCase() // - .input(ESS_SOC, 28) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1 * Math.round(// - Math.min(10_000/* maxApparentPower */ * AbstractFixStateOfCharge.DEFAULT_POWER_FACTOR, + .input("ess0", SOC, 28) // + .output(STATE_MACHINE, State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1 * round(// + min(10_000/* maxApparentPower */ * DEFAULT_POWER_FACTOR, 10_000 /* capacity */ * (1f / 6f))) // 1467 - ))// - ; + )) // + .deactivate(); } @Test public void testBoundaries() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2022-10-27T08:00:00.00Z"), ZoneOffset.UTC); - final var componentManager = new DummyComponentManager(clock); - /* * Below target SoC */ new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("componentManager", componentManager) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("ess", ESS) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(false) // @@ -508,67 +486,67 @@ public void testBoundaries() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(false) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.NOT_STARTED)) // .next(new TestCase() // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .input(ESS_CAPACITY, 10_000) // - .output(STATE_MACHINE, StateMachine.State.BELOW_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -500) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .input("ess0", CAPACITY, 10_000) // + .output(STATE_MACHINE, State.BELOW_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -500) // .output(DEBUG_SET_ACTIVE_POWER_RAW, -500) // .output(DEBUG_SET_ACTIVE_POWER, -500)) // .next(new TestCase() // - .input(ESS_SOC, 22) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1000)) // + .input("ess0", SOC, 22) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1000)) // // Skip Ramp .next(new TestCase(), 17) // .next(new TestCase() // - .input(ESS_SOC, 27) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -10000)) // + .input("ess0", SOC, 27) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -10000)) // .next(new TestCase() // - .input(ESS_SOC, 28)) // + .input("ess0", SOC, 28)) // .next(new TestCase() // - .input(ESS_SOC, 28) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -9500)) // + .input("ess0", SOC, 28) // + .output(STATE_MACHINE, State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -9500)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -9000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -9000)) // // Skip ramp .next(new TestCase(), 13) // .next(new TestCase() // - .input(ESS_SOC, 29) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -2000)) // + .input("ess0", SOC, 29) // + .output(STATE_MACHINE, State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -2000)) // .next(new TestCase() // - .input(ESS_SOC, 29) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1 * Math.round(// - Math.min(10_000/* maxApparentPower */ * AbstractFixStateOfCharge.DEFAULT_POWER_FACTOR, + .input("ess0", SOC, 29) // + .output(STATE_MACHINE, State.WITHIN_LOWER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1 * round(// + min(10_000/* maxApparentPower */ * DEFAULT_POWER_FACTOR, 10_000 /* capacity */ * (1f / 6f))) // ))// 1667 .next(new TestCase() // - .input(ESS_SOC, 30)) // + .input("ess0", SOC, 30)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -667)) // + .input("ess0", SOC, 30) // + .output(STATE_MACHINE, State.AT_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -667)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // + .input("ess0", SOC, 30) // + .output(STATE_MACHINE, State.AT_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // ; /* @@ -576,13 +554,13 @@ public void testBoundaries() throws Exception { */ new ControllerTest(new ControllerEssFixStateOfChargeImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("componentManager", componentManager) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("ess", ESS) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(false) // @@ -590,66 +568,66 @@ public void testBoundaries() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(false) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 40) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 40) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 40) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 40) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 40) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 40) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.NOT_STARTED)) // .next(new TestCase() // - .input(ESS_SOC, 40) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.ABOVE_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 500) // + .input("ess0", SOC, 40) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.ABOVE_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 500) // .output(DEBUG_SET_ACTIVE_POWER_RAW, 500) // .output(DEBUG_SET_ACTIVE_POWER, 500)) // .next(new TestCase() // - .input(ESS_SOC, 40) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 1000)) // + .input("ess0", SOC, 40) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 1000)) // // Skip ramp .next(new TestCase(), 18) // .next(new TestCase() // - .input(ESS_SOC, 33)) // + .input("ess0", SOC, 33)) // .next(new TestCase() // - .input(ESS_SOC, 32)) // + .input("ess0", SOC, 32)) // .next(new TestCase() // - .input(ESS_SOC, 32) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_UPPER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 9500)) // + .input("ess0", SOC, 32) // + .output(STATE_MACHINE, State.WITHIN_UPPER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 9500)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 9000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 9000)) // // Skip ramp .next(new TestCase(), 13) // .next(new TestCase() // - .input(ESS_SOC, 31) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_UPPER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 2000)) // + .input("ess0", SOC, 31) // + .output(STATE_MACHINE, State.WITHIN_UPPER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 2000)) // .next(new TestCase() // - .input(ESS_SOC, 31) // - .output(STATE_MACHINE, StateMachine.State.WITHIN_UPPER_TARGET_SOC_BOUNDARIES) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, Math.round(// - Math.min(10_000/* maxApparentPower */ * AbstractFixStateOfCharge.DEFAULT_POWER_FACTOR, + .input("ess0", SOC, 31) // + .output(STATE_MACHINE, State.WITHIN_UPPER_TARGET_SOC_BOUNDARIES) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, round(// + min(10_000/* maxApparentPower */ * DEFAULT_POWER_FACTOR, 10_000 /* capacity */ * (1f / 6f))) // ))// 1667 .next(new TestCase() // - .input(ESS_SOC, 30)) // + .input("ess0", SOC, 30)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 667)) // + .input("ess0", SOC, 30) // + .output(STATE_MACHINE, State.AT_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 667)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // - ; + .input("ess0", SOC, 30) // + .output(STATE_MACHINE, State.AT_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // + .deactivate(); } @Test @@ -665,8 +643,8 @@ public void testLimitWithSpecifiedTimeBelowLimit() throws Exception { .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("ess", ESS) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(true) // @@ -674,35 +652,35 @@ public void testLimitWithSpecifiedTimeBelowLimit() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(false) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_CAPACITY, 30_000) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 10) // + .input("ess0", CAPACITY, 30_000) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.NOT_STARTED)) // // Start time = 2022-10-27T09:14:24+01:00, Current: 2022-10-27T09:00:00+01:00 .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .input(ESS_CAPACITY, 30_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, null) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .input("ess0", CAPACITY, 30_000) // + .output(STATE_MACHINE, State.NOT_STARTED) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, null) // .output(DEBUG_SET_ACTIVE_POWER_RAW, null) // .output(DEBUG_SET_ACTIVE_POWER, null)) // .next(new TestCase() // - .timeleap(clock, 15, ChronoUnit.MINUTES))// + .timeleap(clock, 15, MINUTES))// .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -500)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -500)) // .next(new TestCase()) // .next(new TestCase()) // .next(new TestCase()) // @@ -712,33 +690,33 @@ public void testLimitWithSpecifiedTimeBelowLimit() throws Exception { .next(new TestCase()) // .next(new TestCase()) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -5000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -5000)) // .next(new TestCase() // - .input(ESS_SOC, 10) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .input(ESS_CAPACITY, 30_000) // - .output(STATE_MACHINE, StateMachine.State.BELOW_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -5040) // + .input("ess0", SOC, 10) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .input("ess0", CAPACITY, 30_000) // + .output(STATE_MACHINE, State.BELOW_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -5040) // .output(DEBUG_SET_ACTIVE_POWER_RAW, -5040) // .output(DEBUG_SET_ACTIVE_POWER, -5040)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .input(ESS_MAX_APPARENT_POWER, 10_000)) // + .input("ess0", SOC, 30) // + .input("ess0", MAX_APPARENT_POWER, 10_000)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -4040)) // + .input("ess0", SOC, 30) // + .output(STATE_MACHINE, State.AT_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -4040)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -3040)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -3040)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -2040)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -2040)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1040)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1040)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -40)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -40)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // - ; + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // + .deactivate(); } @Test @@ -753,8 +731,8 @@ public void testLimitWithSpecifiedTimeAboveLimit() throws Exception { .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("ess", ESS) // .activate(FixStateOfChargeConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setRunning(true) // .setTargetSoc(30) // .setSpecifyTargetTime(true) // @@ -763,85 +741,85 @@ public void testLimitWithSpecifiedTimeAboveLimit() throws Exception { .setSelfTermination(false) // .setTerminationBuffer(720) // .setConditionalTermination(false) // - .setEndCondition(EndCondition.CAPACITY_CHANGED) // + .setEndCondition(CAPACITY_CHANGED) // .build()) .next(new TestCase() // - .input(ESS_SOC, 80) // - .input(ESS_CAPACITY, 30_000) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 80) // + .input("ess0", CAPACITY, 30_000) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 80) // - .input(ESS_CAPACITY, 30_000) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.IDLE)) // + .input("ess0", SOC, 80) // + .input("ess0", CAPACITY, 30_000) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.IDLE)) // .next(new TestCase() // - .input(ESS_SOC, 80) // - .input(ESS_CAPACITY, 30_000) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED)) // + .input("ess0", SOC, 80) // + .input("ess0", CAPACITY, 30_000) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.NOT_STARTED)) // // Start time = 2022-10-27T06:26:24, Current: 2022-10-26T23:00 .next(new TestCase() // - .input(ESS_SOC, 80) // - .input(ESS_CAPACITY, 30_000) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.NOT_STARTED) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, null) // + .input("ess0", SOC, 80) // + .input("ess0", CAPACITY, 30_000) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.NOT_STARTED) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, null) // .output(DEBUG_SET_ACTIVE_POWER_RAW, null) // .output(DEBUG_SET_ACTIVE_POWER, null)) // .next(new TestCase() // - .timeleap(clock, 7, ChronoUnit.HOURS)) // + .timeleap(clock, 7, HOURS)) // .next(new TestCase() // - .timeleap(clock, 31, ChronoUnit.MINUTES)) // + .timeleap(clock, 31, MINUTES)) // .next(new TestCase() // - .input(ESS_SOC, 80) // - .input(ESS_CAPACITY, 30_000) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .output(STATE_MACHINE, StateMachine.State.ABOVE_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 500) // + .input("ess0", SOC, 80) // + .input("ess0", CAPACITY, 30_000) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .output(STATE_MACHINE, State.ABOVE_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 500) // .output(DEBUG_SET_ACTIVE_POWER_RAW, 500) // .output(DEBUG_SET_ACTIVE_POWER, 500)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 1000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 1000)) // .next(new TestCase()) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 2000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 2000)) // .next(new TestCase()) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 3000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 3000)) // .next(new TestCase()) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 4000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 4000)) // .next(new TestCase()) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 5000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 5000)) // .next(new TestCase()) // .next(new TestCase() // - .input(ESS_SOC, 80) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .input(ESS_CAPACITY, 30_000) // - .output(STATE_MACHINE, StateMachine.State.ABOVE_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 5128) // + .input("ess0", SOC, 80) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .input("ess0", CAPACITY, 30_000) // + .output(STATE_MACHINE, State.ABOVE_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 5128) // .output(DEBUG_SET_ACTIVE_POWER_RAW, 5128) // .output(DEBUG_SET_ACTIVE_POWER, 5128)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .input(ESS_MAX_APPARENT_POWER, 10_000)) // + .input("ess0", SOC, 30) // + .input("ess0", MAX_APPARENT_POWER, 10_000)) // .next(new TestCase() // - .input(ESS_SOC, 30) // - .output(STATE_MACHINE, StateMachine.State.AT_TARGET_SOC) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 4128)) // + .input("ess0", SOC, 30) // + .output(STATE_MACHINE, State.AT_TARGET_SOC) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 4128)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 3128)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 3128)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 2128)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 2128)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 1128)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 1128)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 128)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 128)) // .next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)) // - ; + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/bnd.bnd b/io.openems.edge.controller.ess.gridoptimizedcharge/bnd.bnd index ed3817a1757..d00c7cb673d 100644 --- a/io.openems.edge.controller.ess.gridoptimizedcharge/bnd.bnd +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/bnd.bnd @@ -8,10 +8,12 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.common,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ + io.openems.edge.energy.api,\ io.openems.edge.ess.api,\ io.openems.edge.meter.api,\ io.openems.edge.predictor.api,\ io.openems.edge.timedata.api,\ -testpath: \ - ${testpath} + ${testpath},\ + org.apache.commons.math3,\ diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedCharge.java b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedCharge.java index 2ee38fba17a..05dc14dc251 100644 --- a/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedCharge.java +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedCharge.java @@ -192,6 +192,12 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { CONFIGURED_ESS_IS_NOT_MANAGED(Doc.of(Level.FAULT) // .text("The Energy Storage System is in read-only mode and does not allow to be controlled.")), // + /** + * Production values for prediction not available. + */ + NO_VALID_PRODUCTION_PREDICTION(Doc.of(Level.WARNING) // + .translationKey(ControllerEssGridOptimizedCharge.class, "noValidProductionPrediction")), // + /** * Cumulated seconds of the state delay charge. */ @@ -696,6 +702,25 @@ public default void _setConfiguredEssIsNotManaged(Boolean value) { this.getConfiguredEssIsNotManagedChannel().setNextValue(value); } + /** + * Gets the Channel for {@link ChannelId#NO_VALID_PRODUCTION_PREDICTION}. + * + * @return the Channel + */ + public default StateChannel noValidProductionPredictionChannel() { + return this.channel(ChannelId.NO_VALID_PRODUCTION_PREDICTION); + } + + /** + * Internal method to set the 'nextValue' on + * {@link ChannelId#NO_VALID_PRODUCTION_PREDICTION} Channel. + * + * @param value the next value + */ + public default void _setNoValidProductionPredictionChannel(Boolean value) { + this.noValidProductionPredictionChannel().setNextValue(value); + } + /** * Gets the Channel for {@link ChannelId#DELAY_CHARGE_TIME}. * diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImpl.java b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImpl.java index aa727e503b4..dc05f7914c4 100644 --- a/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImpl.java +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImpl.java @@ -1,11 +1,19 @@ package io.openems.edge.controller.ess.gridoptimizedcharge; +import static java.util.stream.Collectors.groupingBy; + +import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZonedDateTime; import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.function.Supplier; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -22,6 +30,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.ImmutableSortedMap; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.edge.common.channel.IntegerReadChannel; @@ -33,6 +43,8 @@ import io.openems.edge.common.filter.RampFilter; import io.openems.edge.common.sum.Sum; import io.openems.edge.controller.api.Controller; +import io.openems.edge.energy.api.EnergySchedulable; +import io.openems.edge.energy.api.EnergyScheduleHandler; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.predictor.api.manager.PredictorManager; @@ -46,8 +58,9 @@ immediate = true, // configurationPolicy = ConfigurationPolicy.REQUIRE // ) -public class ControllerEssGridOptimizedChargeImpl extends AbstractOpenemsComponent implements - ControllerEssGridOptimizedCharge, Controller, OpenemsComponent, TimedataProvider, ComponentManagerProvider { +public class ControllerEssGridOptimizedChargeImpl extends AbstractOpenemsComponent + implements ControllerEssGridOptimizedCharge, EnergySchedulable, Controller, OpenemsComponent, TimedataProvider, + ComponentManagerProvider { /** * Buffer in watt taken into account in the calculation of the first and last @@ -58,6 +71,7 @@ public class ControllerEssGridOptimizedChargeImpl extends AbstractOpenemsCompone protected final RampFilter rampFilter = new RampFilter(); private final Logger log = LoggerFactory.getLogger(ControllerEssGridOptimizedChargeImpl.class); + private final EnergyScheduleHandler energyScheduleHandler; /* * Time counter for the important states @@ -107,6 +121,9 @@ public ControllerEssGridOptimizedChargeImpl() { Controller.ChannelId.values(), // ControllerEssGridOptimizedCharge.ChannelId.values() // ); + this.energyScheduleHandler = buildEnergyScheduleHandler(// + () -> this.config.mode(), // + () -> DelayCharge.parseTime(this.config.manualTargetTime())); } @Activate @@ -145,12 +162,19 @@ private void updateConfig(Config config) { @Override public void run() throws OpenemsNamedException { + if (!this.ess.isManaged() && this.config.mode() != Mode.OFF) { this._setConfiguredEssIsNotManaged(true); return; } this._setConfiguredEssIsNotManaged(false); + if (!this.sum.getProductionActivePower().isDefined()) { + this._setNoValidProductionPredictionChannel(true); + return; + } + this._setNoValidProductionPredictionChannel(false); + // Updates the time channels. this.calculateTime(); @@ -435,4 +459,91 @@ public Timedata getTimedata() { return this.timedata; } + /** + * Builds the {@link EnergyScheduleHandler}. + * + *

+ * This is public so that it can be used by the EnergyScheduler integration + * test. + * + * @param mode a supplier for the configured {@link Mode} + * @param manualTargetTime a supplier for the configured manualTargetTime + * @return a {@link EnergyScheduleHandler} + */ + public static EnergyScheduleHandler buildEnergyScheduleHandler(Supplier mode, + Supplier manualTargetTime) { + return EnergyScheduleHandler.of(// + simContext -> { + // TODO try to reuse existing logic for parsing, calculating limits, etc.; for + // now this only works for current day and MANUAL mode + final var limits = ImmutableSortedMap.naturalOrder(); + final var periodsPerDay = simContext.periods().stream() // + .collect(groupingBy(p -> p.time().truncatedTo(ChronoUnit.DAYS))); + if (!periodsPerDay.isEmpty()) { + final var firstDayMignight = Collections.min(periodsPerDay.keySet()); + + for (var entry : periodsPerDay.entrySet()) { + // Find target time for this day + var midnight = entry.getKey(); // beginning of this day + var periods = entry.getValue(); // periods of this day + ZonedDateTime targetTime = switch (mode.get()) { + case OFF -> midnight; // Can not happen + case MANUAL -> midnight // + .withHour(manualTargetTime.get().getHour()) // + .withMinute(manualTargetTime.get().getMinute()); + case AUTOMATIC -> midnight; // TODO + }; + // Find first period with Production > Consumption + var firstExcessEnergyOpt = periods.stream() // + .filter(p -> p.production() > p.consumption()) // + .findFirst(); + if (firstExcessEnergyOpt.isEmpty() + || targetTime.isBefore(firstExcessEnergyOpt.get().time())) { + // Production exceeds Consumption never or too late on this day + // -> set no limit for this day + limits.put(midnight, OptionalInt.empty()); + continue; + } + var firstExcessEnergy = firstExcessEnergyOpt.get().time(); + + // Set no limit for early hours of the day + if (firstExcessEnergy.isAfter(midnight)) { + limits.put(midnight, OptionalInt.empty()); + } + + // Calculate actual charge limit + var noOfQuarters = (int) Duration.between(firstExcessEnergy, targetTime).toMinutes() / 15; + final var totalEnergy = midnight == firstDayMignight // + ? // use actual data for first day + simContext.ess().totalEnergy() - simContext.ess().currentEnergy() + : // assume full charge from second day + simContext.ess().totalEnergy(); + limits.put(firstExcessEnergy, OptionalInt.of(totalEnergy / noOfQuarters)); + + // No limit after targetTime + limits.put(targetTime, OptionalInt.empty()); + } + } + + return new EshContext(limits.build()); + }, // + (simContext, period, energyFlow, ctrlContext) -> { + var limitEntry = ctrlContext.limits.floorEntry(period.time()); + if (limitEntry == null) { + return; + } + var limit = limitEntry.getValue(); + if (limit.isPresent()) { + energyFlow.setEssMaxCharge(limit.getAsInt()); + } + }); + } + + private static record EshContext(ImmutableSortedMap limits) { + } + + @Override + public EnergyScheduleHandler getEnergyScheduleHandler() { + return this.energyScheduleHandler; + } } diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/package-info.java b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/package-info.java new file mode 100644 index 00000000000..9736f9da6be --- /dev/null +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.controller.ess.gridoptimizedcharge; diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/translation_de.properties b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/translation_de.properties new file mode 100644 index 00000000000..2660bc97b77 --- /dev/null +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/translation_de.properties @@ -0,0 +1,2 @@ +# ControllerEssGridOptimizedCharge +noValidProductionPrediction = Keine Erzeugungsprognose möglich. Bitte erfassen Sie die Erzeugung Ihrer Anlage über eine App oder wählen Sie in der netzdienlichen Beladung den Modus 'AUS'. diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/translation_en.properties b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/translation_en.properties new file mode 100644 index 00000000000..e2195067037 --- /dev/null +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/translation_en.properties @@ -0,0 +1,2 @@ +# ControllerEssGridOptimizedCharge +noValidProductionPrediction = No production forecast available. Please log the production via an app or set the grid-optimized charge mode to 'OFF'. diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImplTest.java b/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImplTest.java index 03f197139e3..862b0955d56 100644 --- a/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImplTest.java +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImplTest.java @@ -1,5 +1,20 @@ package io.openems.edge.controller.ess.gridoptimizedcharge; +import static io.openems.edge.common.sum.Sum.ChannelId.PRODUCTION_DC_ACTUAL_POWER; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.DELAY_CHARGE_STATE; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.PREDICTED_TARGET_MINUTE; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.PREDICTED_TARGET_MINUTE_ADJUSTED; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.RAW_DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.SELL_TO_GRID_LIMIT_STATE; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.START_EPOCH_SECONDS; +import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedCharge.ChannelId.TARGET_MINUTE; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_LESS_OR_EQUALS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.CAPACITY; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.MAX_APPARENT_POWER; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.SOC; import static io.openems.edge.predictor.api.prediction.Prediction.EMPTY_PREDICTION; import static java.time.temporal.ChronoUnit.DAYS; import static org.junit.Assert.assertEquals; @@ -34,8 +49,10 @@ import io.openems.edge.common.test.Plot.AxisFormat; import io.openems.edge.common.test.Plot.Data; import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyHybridEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.test.DummyElectricityMeter; import io.openems.edge.predictor.api.prediction.Prediction; import io.openems.edge.predictor.api.test.DummyPredictor; @@ -43,50 +60,13 @@ public class ControllerEssGridOptimizedChargeImplTest { - // Ids - private static final String CTRL_ID = "ctrlGridOptimizedCharge0"; - private static final String PREDICTOR_ID = "predictor0"; - private static final String ESS_ID = "ess0"; - private static final String METER_ID = "meter0"; - // Components - private static final DummyManagedSymmetricEss ESS = new DummyManagedSymmetricEss(ESS_ID); - private static final DummyElectricityMeter METER = new DummyElectricityMeter(METER_ID); - private static final DummyHybridEss HYBRID_ESS = new DummyHybridEss(ESS_ID); - private static final DummyManagedSymmetricEss ESS_WITH_NONE_APPARENT_POWER = new DummyManagedSymmetricEss(ESS_ID) // + private static final DummyManagedSymmetricEss ESS = new DummyManagedSymmetricEss("ess0"); + private static final DummyElectricityMeter METER = new DummyElectricityMeter("meter0"); + private static final DummyHybridEss HYBRID_ESS = new DummyHybridEss("ess0"); + private static final DummyManagedSymmetricEss ESS_WITH_NONE_APPARENT_POWER = new DummyManagedSymmetricEss("ess0") // .withMaxApparentPower(0); - // Ess channels - private static final ChannelAddress ESS_CAPACITY = new ChannelAddress(ESS_ID, "Capacity"); - private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); - private static final ChannelAddress ESS_MAX_APPARENT_POWER = new ChannelAddress(ESS_ID, "MaxApparentPower"); - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerLessOrEquals"); - - // Meter channels - private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - - // Controller channels - private static final ChannelAddress PREDICTED_TARGET_MINUTE = new ChannelAddress(CTRL_ID, "PredictedTargetMinute"); - private static final ChannelAddress PREDICTED_TARGET_MINUTE_ADJUSTED = new ChannelAddress(CTRL_ID, - "PredictedTargetMinuteAdjusted"); - private static final ChannelAddress TARGET_MINUTE = new ChannelAddress(CTRL_ID, "TargetMinute"); - private static final ChannelAddress DELAY_CHARGE_STATE = new ChannelAddress(CTRL_ID, "DelayChargeState"); - private static final ChannelAddress SELL_TO_GRID_LIMIT_STATE = new ChannelAddress(CTRL_ID, "SellToGridLimitState"); - private static final ChannelAddress DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT = new ChannelAddress(CTRL_ID, - "DelayChargeMaximumChargeLimit"); - private static final ChannelAddress RAW_DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT = new ChannelAddress(CTRL_ID, - "RawDelayChargeMaximumChargeLimit"); - private static final ChannelAddress SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT = new ChannelAddress(CTRL_ID, - "SellToGridLimitMinimumChargeLimit"); - private static final ChannelAddress RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT = new ChannelAddress(CTRL_ID, - "RawSellToGridLimitChargeLimit"); - private static final ChannelAddress START_EPOCH_SECONDS = new ChannelAddress(CTRL_ID, "StartEpochSeconds"); - - // Sum channels - private static final ChannelAddress SUM_PRODUCTION_DC_ACTUAL_POWER = new ChannelAddress("_sum", - "ProductionDcActualPower"); private static final ChannelAddress SUM_PRODUCTION_ACTIVE_POWER = new ChannelAddress("_sum", "ProductionActivePower"); private static final ChannelAddress SUM_CONSUMPTION_ACTIVE_POWER = new ChannelAddress("_sum", @@ -157,11 +137,11 @@ public void automatic_default_predictions_at_midnight_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, now, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, now, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -173,10 +153,10 @@ public void automatic_default_predictions_at_midnight_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.AUTOMATIC) // .setSellToGridLimitEnabled(true) // @@ -184,18 +164,20 @@ public void automatic_default_predictions_at_midnight_test() throws Exception { .setManualTargetTime("") // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // .input(START_EPOCH_SECONDS, 1630566000) // .output(PREDICTED_TARGET_MINUTE, /* QuarterHour */ 68 * 15) // .output(PREDICTED_TARGET_MINUTE_ADJUSTED, /* QuarterHour */ 68 * 15 - 120) // .output(DELAY_CHARGE_STATE, DelayChargeState.AVOID_LOW_CHARGING) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, -6650) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, 6650) // - .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 0)); // Avoid low charge power + .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 0)) // Avoid low charge power + .deactivate(); } @Test @@ -206,11 +188,11 @@ public void automatic_default_predictions_at_midday_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, midnight, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, midnight, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -222,10 +204,10 @@ public void automatic_default_predictions_at_midday_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.AUTOMATIC) // .setSellToGridLimitEnabled(true) // @@ -233,11 +215,12 @@ public void automatic_default_predictions_at_midday_test() throws Exception { .setManualTargetTime("") // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // .input(START_EPOCH_SECONDS, 1630566000) // .output(PREDICTED_TARGET_MINUTE, /* QuarterHour */ 68 * 15) // .output(PREDICTED_TARGET_MINUTE_ADJUSTED, /* QuarterHour */ 68 * 15 - 120) // @@ -246,7 +229,8 @@ public void automatic_default_predictions_at_midday_test() throws Exception { .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, 6650) // // If Energy calculation would be applied on medium risk level - Predicted // available Energy is not enough to reach 100% - .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2700)); + .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2700)) // + .deactivate(); } @Test @@ -262,11 +246,11 @@ public void automatic_default_predictions_at_midday_averaged_test() throws Excep final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, midnight, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, midnight, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -278,10 +262,10 @@ public void automatic_default_predictions_at_midday_averaged_test() throws Excep .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.AUTOMATIC) // .setSellToGridLimitEnabled(true) // @@ -290,11 +274,12 @@ public void automatic_default_predictions_at_midday_averaged_test() throws Excep .build()) // .next(new TestCase() // .onAfterProcessImage(sleep) // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // .input(START_EPOCH_SECONDS, 1630566000) // .output(PREDICTED_TARGET_MINUTE, /* QuarterHour */ 68 * 15) // .output(PREDICTED_TARGET_MINUTE_ADJUSTED, /* QuarterHour */ 68 * 15 - 120) // @@ -305,23 +290,28 @@ public void automatic_default_predictions_at_midday_averaged_test() throws Excep .output(RAW_DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2700)) // .next(new TestCase() // .onAfterProcessImage(sleep) // - .input(ESS_SOC, 21) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("ess0", SOC, 21) // .input(START_EPOCH_SECONDS, 1630566000) // .output(DELAY_CHARGE_STATE, DelayChargeState.ACTIVE_LIMIT) // .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2683) // .output(RAW_DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2666)) // .next(new TestCase() // .onAfterProcessImage(sleep) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2677) // .output(RAW_DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2666)) // .next(new TestCase() // .onAfterProcessImage(sleep) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2675) // .output(RAW_DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2666)) // .next(new TestCase() // .onAfterProcessImage(sleep) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2673) // .output(RAW_DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2666)) // + .deactivate(); ; } @@ -333,11 +323,11 @@ public void automatic_default_predictions_at_evening_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, now, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, now, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -349,10 +339,10 @@ public void automatic_default_predictions_at_evening_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.AUTOMATIC) // .setSellToGridLimitEnabled(true) // @@ -360,11 +350,12 @@ public void automatic_default_predictions_at_evening_test() throws Exception { .setManualTargetTime("") // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // .input(PREDICTED_TARGET_MINUTE, /* QuarterHour */ 68 * 15) // .input(START_EPOCH_SECONDS, 1630566000) // .input(PREDICTED_TARGET_MINUTE_ADJUSTED, /* QuarterHour */ 68 * 15 - 120) // @@ -373,6 +364,7 @@ public void automatic_default_predictions_at_evening_test() throws Exception { // Value increases steadily by 0.25% of max apparent power 10_000 .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2025)) .next(new TestCase() // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // .input(PREDICTED_TARGET_MINUTE, /* QuarterHour */ 68 * 15) // .input(PREDICTED_TARGET_MINUTE_ADJUSTED, /* QuarterHour */ 68 * 15 - 120) // .input(START_EPOCH_SECONDS, 1630566000) // @@ -383,6 +375,7 @@ public void automatic_default_predictions_at_evening_test() throws Exception { .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, 6650) // .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2050)) .next(new TestCase() // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // .input(PREDICTED_TARGET_MINUTE, /* QuarterHour */ 68 * 15) // .input(PREDICTED_TARGET_MINUTE_ADJUSTED, /* QuarterHour */ 68 * 15 - 120) // .input(START_EPOCH_SECONDS, 1630566000) // @@ -391,7 +384,8 @@ public void automatic_default_predictions_at_evening_test() throws Exception { .output(DELAY_CHARGE_STATE, DelayChargeState.ACTIVE_LIMIT) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, -6650) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, 6650) // - .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2075)); + .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 2075)) // + .deactivate(); } @Test @@ -399,8 +393,8 @@ public void automatic_no_predictions_test() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2020-01-01T00:00:00.00Z"), ZoneOffset.UTC); final var cm = new DummyComponentManager(clock); final var predictorManager = new DummyPredictorManager( - new DummyPredictor(PREDICTOR_ID, cm, EMPTY_PREDICTION, SUM_PRODUCTION_ACTIVE_POWER), // - new DummyPredictor(PREDICTOR_ID, cm, EMPTY_PREDICTION, SUM_CONSUMPTION_ACTIVE_POWER)); + new DummyPredictor("predictor0", cm, EMPTY_PREDICTION, SUM_PRODUCTION_ACTIVE_POWER), // + new DummyPredictor("predictor0", cm, EMPTY_PREDICTION, SUM_CONSUMPTION_ACTIVE_POWER)); new ControllerTest(new ControllerEssGridOptimizedChargeImpl()) // .addReference("predictorManager", predictorManager) // @@ -410,10 +404,10 @@ public void automatic_no_predictions_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.AUTOMATIC) // .setSellToGridLimitEnabled(true) // @@ -421,15 +415,17 @@ public void automatic_no_predictions_test() throws Exception { .setManualTargetTime("") // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // .input(START_EPOCH_SECONDS, 1630566000) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, -6650) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, 6650) // - .output(DELAY_CHARGE_STATE, DelayChargeState.TARGET_MINUTE_NOT_CALCULATED)); + .output(DELAY_CHARGE_STATE, DelayChargeState.TARGET_MINUTE_NOT_CALCULATED)) // + .deactivate(); } @Test @@ -437,8 +433,8 @@ public void automatic_sell_to_grid_limit_test() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2020-01-01T00:00:00.00Z"), ZoneOffset.UTC); final var cm = new DummyComponentManager(clock); final var predictorManager = new DummyPredictorManager( - new DummyPredictor(PREDICTOR_ID, cm, EMPTY_PREDICTION, SUM_PRODUCTION_ACTIVE_POWER), // - new DummyPredictor(PREDICTOR_ID, cm, EMPTY_PREDICTION, SUM_CONSUMPTION_ACTIVE_POWER)); + new DummyPredictor("predictor0", cm, EMPTY_PREDICTION, SUM_PRODUCTION_ACTIVE_POWER), // + new DummyPredictor("predictor0", cm, EMPTY_PREDICTION, SUM_CONSUMPTION_ACTIVE_POWER)); new ControllerTest(new ControllerEssGridOptimizedChargeImpl()) // .addReference("predictorManager", predictorManager) // @@ -448,10 +444,10 @@ public void automatic_sell_to_grid_limit_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.AUTOMATIC) // .setSellToGridLimitEnabled(true) // @@ -459,68 +455,76 @@ public void automatic_sell_to_grid_limit_test() throws Exception { .setManualTargetTime("") // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -7500) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .input(ESS_ACTIVE_POWER, 0) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7500) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // .input(START_EPOCH_SECONDS, 1630566000) // .output(DELAY_CHARGE_STATE, DelayChargeState.TARGET_MINUTE_NOT_CALCULATED) // .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, null) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -850) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -850) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -850) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 850) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -12000) // - .input(ESS_ACTIVE_POWER, -850) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -12000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -850) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6200) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6200) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6200) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6200) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -7000) // - .input(ESS_ACTIVE_POWER, -6200) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6200) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6550) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6550) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6550) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6550) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -5000) // - .input(ESS_ACTIVE_POWER, -6550) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -5000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6550) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6050) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6050) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6050) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6050) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -8000) // - .input(ESS_ACTIVE_POWER, -6050) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -8000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6050) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -7400) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -7400) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -7400) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 7400) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // // Difference between last limit and current lower than the ramp - ramp is not // applied - .input(METER_ACTIVE_POWER, -7000) // - .input(ESS_ACTIVE_POWER, -7400) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -7400) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -7750) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -7750) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -7750) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 7750) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -6000) // - .input(ESS_ACTIVE_POWER, -7750) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -6000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -7750) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -7250) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -7250) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -7250) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 7250) // - .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)); + .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // + .deactivate(); } @Test @@ -528,8 +532,8 @@ public void automatic_sell_to_grid_limit_test_with_full_ess() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2020-01-01T00:00:00.00Z"), ZoneOffset.UTC); final var cm = new DummyComponentManager(clock); final var predictorManager = new DummyPredictorManager( - new DummyPredictor(PREDICTOR_ID, cm, EMPTY_PREDICTION, SUM_PRODUCTION_ACTIVE_POWER), // - new DummyPredictor(PREDICTOR_ID, cm, EMPTY_PREDICTION, SUM_CONSUMPTION_ACTIVE_POWER)); + new DummyPredictor("predictor0", cm, EMPTY_PREDICTION, SUM_PRODUCTION_ACTIVE_POWER), // + new DummyPredictor("predictor0", cm, EMPTY_PREDICTION, SUM_CONSUMPTION_ACTIVE_POWER)); new ControllerTest(new ControllerEssGridOptimizedChargeImpl()) // .addReference("predictorManager", predictorManager) // @@ -539,10 +543,10 @@ public void automatic_sell_to_grid_limit_test_with_full_ess() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.AUTOMATIC) // .setSellToGridLimitEnabled(true) // @@ -550,70 +554,78 @@ public void automatic_sell_to_grid_limit_test_with_full_ess() throws Exception { .setManualTargetTime("") // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -7500) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 100) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .input(ESS_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7500) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 100) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // .input(START_EPOCH_SECONDS, 1630566000) // .output(DELAY_CHARGE_STATE, DelayChargeState.TARGET_MINUTE_NOT_CALCULATED) // .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, null) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -500) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -500) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -500) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 500) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -12000) // - .input(ESS_ACTIVE_POWER, -1000) // - .input(ESS_SOC, 100) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -12000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -1000) // + .input("ess0", SOC, 100) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6000) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6000) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6000) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6000) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -7000) // - .input(ESS_ACTIVE_POWER, -6000) // - .input(ESS_SOC, 100) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6000) // + .input("ess0", SOC, 100) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6000) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6000) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6000) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6000) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -5000) // - .input(ESS_ACTIVE_POWER, -6000) // - .input(ESS_SOC, 100) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -5500) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -5000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6000) // + .input("ess0", SOC, 100) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -5500) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -5500) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 5500) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -8000) // - .input(ESS_ACTIVE_POWER, -5500) // - .input(ESS_SOC, 100) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6500) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -8000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -5500) // + .input("ess0", SOC, 100) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6500) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6500) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6500) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // // Difference between last limit and current lower than the ramp - ramp is not // applied - .input(METER_ACTIVE_POWER, -7000) // - .input(ESS_ACTIVE_POWER, -6300) // - .input(ESS_SOC, 100) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6300) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6300) // + .input("ess0", SOC, 100) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6300) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6300) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6300) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -6000) // - .input(ESS_ACTIVE_POWER, -6000) // - .input(ESS_SOC, 100) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -5800) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -6000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6000) // + .input("ess0", SOC, 100) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -5800) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -5800) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 5800) // - .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)); + .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // + .deactivate(); } @Test @@ -621,8 +633,8 @@ public void automatic_sell_to_grid_limit_buffer_test() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2020-01-01T00:00:00.00Z"), ZoneOffset.UTC); final var cm = new DummyComponentManager(clock); final var predictorManager = new DummyPredictorManager( - new DummyPredictor(PREDICTOR_ID, cm, EMPTY_PREDICTION, SUM_PRODUCTION_ACTIVE_POWER), // - new DummyPredictor(PREDICTOR_ID, cm, EMPTY_PREDICTION, SUM_CONSUMPTION_ACTIVE_POWER)); + new DummyPredictor("predictor0", cm, EMPTY_PREDICTION, SUM_PRODUCTION_ACTIVE_POWER), // + new DummyPredictor("predictor0", cm, EMPTY_PREDICTION, SUM_CONSUMPTION_ACTIVE_POWER)); new ControllerTest(new ControllerEssGridOptimizedChargeImpl()) // .addReference("predictorManager", predictorManager) // @@ -632,10 +644,10 @@ public void automatic_sell_to_grid_limit_buffer_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.AUTOMATIC) // .setSellToGridLimitEnabled(true) // @@ -643,68 +655,76 @@ public void automatic_sell_to_grid_limit_buffer_test() throws Exception { .setManualTargetTime("") // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -7500) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .input(ESS_ACTIVE_POWER, 0) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7500) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // .input(START_EPOCH_SECONDS, 1630566000) // .output(DELAY_CHARGE_STATE, DelayChargeState.TARGET_MINUTE_NOT_CALCULATED) // .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, null) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -850) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -850) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -850) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 850) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -12000) // - .input(ESS_ACTIVE_POWER, -1000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -12000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -1000) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6350) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6350) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6350) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6350) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -7000) // - .input(ESS_ACTIVE_POWER, -6000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6000) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6350) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6350) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6350) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6350) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -5000) // - .input(ESS_ACTIVE_POWER, -6000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -5000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6000) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -5850) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -5850) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -5850) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 5850) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -8000) // - .input(ESS_ACTIVE_POWER, -5500) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -8000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -5500) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6850) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6850) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6850) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6850) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // // Difference between last limit and current lower than the ramp - ramp is not // applied - .input(METER_ACTIVE_POWER, -7000) // - .input(ESS_ACTIVE_POWER, -6300) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6300) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6650) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6650) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6650) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6650) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -6000) // - .input(ESS_ACTIVE_POWER, -6000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -6000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, -6000) // .input(START_EPOCH_SECONDS, 1630566000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -6150) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -6150) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -6150) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 6150) // - .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)); + .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT)) // + .deactivate(); } @Test @@ -715,11 +735,11 @@ public void manual_midnight_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, now, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, now, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -731,10 +751,10 @@ public void manual_midnight_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.MANUAL) // .setSellToGridLimitEnabled(true) // @@ -742,19 +762,21 @@ public void manual_midnight_test() throws Exception { .setSellToGridLimitRampPercentage(5) // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // .input(START_EPOCH_SECONDS, 1630566000) // - .input(ESS_MAX_APPARENT_POWER, 10_000)) // + .input("ess0", MAX_APPARENT_POWER, 10_000)) // .next(new TestCase() // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // .input(START_EPOCH_SECONDS, 1630566000) // .output(TARGET_MINUTE, /* QuarterHour */ 1020) // .output(DELAY_CHARGE_STATE, DelayChargeState.AVOID_LOW_CHARGING) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, 6650) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, -6650) // - .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 0)); // 476 W below minimum + .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 0)) // 476 W below minimum + .deactivate(); } @Test @@ -765,11 +787,11 @@ public void manual_midday_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, now, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, now, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -781,10 +803,10 @@ public void manual_midday_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.MANUAL) // .setSellToGridLimitEnabled(true) // @@ -792,17 +814,19 @@ public void manual_midday_test() throws Exception { .setSellToGridLimitRampPercentage(5) // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // .input(START_EPOCH_SECONDS, 1630566000) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // .output(TARGET_MINUTE, /* QuarterHour */ 1020) // .output(DELAY_CHARGE_STATE, DelayChargeState.ACTIVE_LIMIT) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, 6650) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, -6650) // - .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 1620)); + .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 1620)) // + .deactivate(); } @Test @@ -813,11 +837,11 @@ public void hybridEss_manual_midday_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, now, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, now, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -829,10 +853,10 @@ public void hybridEss_manual_midday_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.MANUAL) // .setSellToGridLimitEnabled(true) // @@ -840,18 +864,21 @@ public void hybridEss_manual_midday_test() throws Exception { .setSellToGridLimitRampPercentage(5) // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // .input(START_EPOCH_SECONDS, 1630566000) // - .input(SUM_PRODUCTION_DC_ACTUAL_POWER, 10_000).output(TARGET_MINUTE, /* QuarterHour */ 1020) // + .input(PRODUCTION_DC_ACTUAL_POWER, 10_000) // + .output(TARGET_MINUTE, /* QuarterHour */ 1020) // .output(DELAY_CHARGE_STATE, DelayChargeState.NO_CHARGE_LIMIT) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 3350) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, 6650) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_FIXED) // - .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, null)); + .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, null)) // + .deactivate(); } @@ -863,11 +890,11 @@ public void mode_off_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, now, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, now, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -879,10 +906,10 @@ public void mode_off_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.OFF) // .setSellToGridLimitEnabled(true) // @@ -890,16 +917,18 @@ public void mode_off_test() throws Exception { .setManualTargetTime("") // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -7500) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // - .input(ESS_ACTIVE_POWER, 0) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -7500) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // .input(START_EPOCH_SECONDS, 1630566000) // .output(DELAY_CHARGE_STATE, DelayChargeState.DISABLED) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.ACTIVE_LIMIT_CONSTRAINT) // .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, null) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, -850) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -850) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, -850) // - .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 850)); // + .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, 850)) // + .deactivate(); } @Test @@ -910,11 +939,11 @@ public void no_capacity_left_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, now, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, now, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -926,10 +955,10 @@ public void no_capacity_left_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.AUTOMATIC) // .setSellToGridLimitEnabled(true) // @@ -937,18 +966,20 @@ public void no_capacity_left_test() throws Exception { .setManualTargetTime("") // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 99) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input(SUM_PRODUCTION_ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 99) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // .input(START_EPOCH_SECONDS, 1630566000) // // ess.getPower().getMinPower() (Maximum allowed charge power) is '0' because // the referenced // DummyManagedSymmetricEss has an apparent power of zero. .output(DELAY_CHARGE_STATE, DelayChargeState.NO_REMAINING_CAPACITY) // - .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, null)); // + .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, null)) // + .deactivate(); } @Test @@ -964,11 +995,11 @@ public void start_production_not_enough_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, now, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, now, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -980,10 +1011,10 @@ public void start_production_not_enough_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.OFF) // .setSellToGridLimitEnabled(true) // @@ -1010,7 +1041,8 @@ public void start_production_not_enough_test() throws Exception { .input(SUM_CONSUMPTION_ACTIVE_POWER, 6000) // .output(DELAY_CHARGE_STATE, DelayChargeState.NOT_STARTED) // .output(SELL_TO_GRID_LIMIT_STATE, SellToGridLimitState.NOT_STARTED) // - .output(START_EPOCH_SECONDS, null)); // + .output(START_EPOCH_SECONDS, null)) // + .deactivate(); } @Test @@ -1026,11 +1058,11 @@ public void start_production_average_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, now, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, now, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); @@ -1042,10 +1074,10 @@ public void start_production_average_test() throws Exception { .addReference("meter", METER) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setEssId(ESS_ID) // - .setId(CTRL_ID) // + .setEssId("ess0") // + .setId("ctrlGridOptimizedCharge0") // .setMaximumSellToGridPower(7_000) // - .setMeterId(METER_ID) // + .setMeterId("meter0") // .setDelayChargeRiskLevel(DelayChargeRiskLevel.MEDIUM) // .setMode(Mode.MANUAL) // .setSellToGridLimitEnabled(true) // @@ -1151,11 +1183,11 @@ public void start_production_average_test() throws Exception { .input(SUM_PRODUCTION_ACTIVE_POWER, 2000) // Avg: 1166 .input(SUM_CONSUMPTION_ACTIVE_POWER, 1000) // .input(START_EPOCH_SECONDS, null) // - .input(METER_ACTIVE_POWER, 0) // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_CAPACITY, 10_000) // - .input(ESS_SOC, 20) // - .input(ESS_MAX_APPARENT_POWER, 10_000) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", CAPACITY, 10_000) // + .input("ess0", SOC, 20) // + .input("ess0", MAX_APPARENT_POWER, 10_000) // // Epoch seconds at 2020-01-01 00:00:00: 1577836800 (Clock is not updated) .output(START_EPOCH_SECONDS, 1577836800L) // @@ -1163,12 +1195,12 @@ public void start_production_average_test() throws Exception { .output(DELAY_CHARGE_STATE, DelayChargeState.AVOID_LOW_CHARGING) // .output(RAW_SELL_TO_GRID_LIMIT_CHARGE_LIMIT, 6650) // .output(SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT, -6650) // - .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 0)); // 506 W is not efficient + .output(DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT, 0)) // 506 W is not efficient + .deactivate(); } @Test public void getCalculatedPowerLimit_middayTest() throws Exception { - /* * Initial values */ @@ -1412,80 +1444,6 @@ private void testLogic(String description, Integer[] productionPrediction, Integ }); } - private DelayChargeResultState testOneDay(String testDescription, Integer[] productionPrediction, - Integer[] consumptionPrediction, int soc, Optional targetMinuteOpt, int capacity, - int maxApparentPower, int allowedChargePower, DelayChargeRiskLevel riskLevel, Integer[] productionActual, - Integer[] consumptionActual, float resultBuffer) { - DelayChargeResultState resultState; - DelayChargeResult newLogic = ControllerEssGridOptimizedChargeImplTest.testOneDay(testDescription, - productionPrediction, consumptionPrediction, soc, targetMinuteOpt, capacity, maxApparentPower, - allowedChargePower, riskLevel, productionActual, consumptionActual, false); - - DelayChargeResult oldLogic = ControllerEssGridOptimizedChargeImplTest.testOneDay(testDescription, - productionPrediction, consumptionPrediction, soc, targetMinuteOpt, capacity, maxApparentPower, - allowedChargePower, riskLevel, productionActual, consumptionActual, true); - - if (newLogic.getFinalSoc() + resultBuffer < oldLogic.getFinalSoc()) { - resultState = DelayChargeResultState.WARNING; - } else if (newLogic.getFinalSoc() - resultBuffer > oldLogic.getFinalSoc()) { - resultState = DelayChargeResultState.PERFECT; - } else { - resultState = DelayChargeResultState.OK; - } - - float unefficientEnergy = Math - .round(newLogic.getChargedEnergyWithLowPower() / newLogic.getChargedEnergy() * 1000) / 10.0f; - float unefficientEnergyOld = Math - .round(oldLogic.getChargedEnergyWithLowPower() / oldLogic.getChargedEnergy() * 1000) / 10.0f; - System.out.println(resultState.text + "\t" + testDescription + " \t(New: " - + Math.round(newLogic.getFinalSoc() * 100) / 100.0 + " | Old: " - + Math.round(oldLogic.getFinalSoc() * 100) / 100.0 + ") \t Energy: (New: " - + newLogic.getChargedEnergy() + "[" + newLogic.getChargedEnergyWithLowPower() + " -> " - + unefficientEnergy + "%] | Old: " + oldLogic.getChargedEnergy() + "[" - + oldLogic.getChargedEnergyWithLowPower() + " -> " + unefficientEnergyOld + "%])"); - - // fail("New logic results in a lower SoC (New: " + newLogic.getFinalSoc() + " | - // Old: "+ oldLogic.getFinalSoc() + ") - " + testDescription); - return resultState; - } - - private static class DelayChargeResult { - - private float finalSoc; - private float chargedEnergy; - private float chargedEnergyWithLowPower; - - public DelayChargeResult(float finalSoc, float chargedEnergy, float chargedEnergyWithLowPower) { - this.finalSoc = finalSoc; - this.chargedEnergy = chargedEnergy; - this.chargedEnergyWithLowPower = chargedEnergyWithLowPower; - } - - public float getFinalSoc() { - return this.finalSoc; - } - - public float getChargedEnergy() { - return this.chargedEnergy; - } - - public float getChargedEnergyWithLowPower() { - return this.chargedEnergyWithLowPower; - } - } - - private static enum DelayChargeResultState { - OK("OK - SoC as bevore"), // - WARNING("WARNING - Lower SoC"), // - PERFECT("PERFECT - Higher SoC"); - - private String text; - - DelayChargeResultState(String text) { - this.text = text; - } - } - @SuppressWarnings("deprecation") private static DelayChargeResult testOneDay(String testDescription, Integer[] productionPrediction, Integer[] consumptionPrediction, int soc, Optional targetMinuteOpt, int capacity, @@ -1626,6 +1584,80 @@ private static DelayChargeResult testOneDay(String testDescription, Integer[] pr return new DelayChargeResult(socFloat, totoalActivePower * 0.25f, totoalActivePowerLessEfficiency * 0.25f); } + private DelayChargeResultState testOneDay(String testDescription, Integer[] productionPrediction, + Integer[] consumptionPrediction, int soc, Optional targetMinuteOpt, int capacity, + int maxApparentPower, int allowedChargePower, DelayChargeRiskLevel riskLevel, Integer[] productionActual, + Integer[] consumptionActual, float resultBuffer) { + DelayChargeResultState resultState; + DelayChargeResult newLogic = ControllerEssGridOptimizedChargeImplTest.testOneDay(testDescription, + productionPrediction, consumptionPrediction, soc, targetMinuteOpt, capacity, maxApparentPower, + allowedChargePower, riskLevel, productionActual, consumptionActual, false); + + DelayChargeResult oldLogic = ControllerEssGridOptimizedChargeImplTest.testOneDay(testDescription, + productionPrediction, consumptionPrediction, soc, targetMinuteOpt, capacity, maxApparentPower, + allowedChargePower, riskLevel, productionActual, consumptionActual, true); + + if (newLogic.getFinalSoc() + resultBuffer < oldLogic.getFinalSoc()) { + resultState = DelayChargeResultState.WARNING; + } else if (newLogic.getFinalSoc() - resultBuffer > oldLogic.getFinalSoc()) { + resultState = DelayChargeResultState.PERFECT; + } else { + resultState = DelayChargeResultState.OK; + } + + float unefficientEnergy = Math + .round(newLogic.getChargedEnergyWithLowPower() / newLogic.getChargedEnergy() * 1000) / 10.0f; + float unefficientEnergyOld = Math + .round(oldLogic.getChargedEnergyWithLowPower() / oldLogic.getChargedEnergy() * 1000) / 10.0f; + System.out.println(resultState.text + "\t" + testDescription + " \t(New: " + + Math.round(newLogic.getFinalSoc() * 100) / 100.0 + " | Old: " + + Math.round(oldLogic.getFinalSoc() * 100) / 100.0 + ") \t Energy: (New: " + + newLogic.getChargedEnergy() + "[" + newLogic.getChargedEnergyWithLowPower() + " -> " + + unefficientEnergy + "%] | Old: " + oldLogic.getChargedEnergy() + "[" + + oldLogic.getChargedEnergyWithLowPower() + " -> " + unefficientEnergyOld + "%])"); + + // fail("New logic results in a lower SoC (New: " + newLogic.getFinalSoc() + " | + // Old: "+ oldLogic.getFinalSoc() + ") - " + testDescription); + return resultState; + } + + private static class DelayChargeResult { + + private float finalSoc; + private float chargedEnergy; + private float chargedEnergyWithLowPower; + + public DelayChargeResult(float finalSoc, float chargedEnergy, float chargedEnergyWithLowPower) { + this.finalSoc = finalSoc; + this.chargedEnergy = chargedEnergy; + this.chargedEnergyWithLowPower = chargedEnergyWithLowPower; + } + + public float getFinalSoc() { + return this.finalSoc; + } + + public float getChargedEnergy() { + return this.chargedEnergy; + } + + public float getChargedEnergyWithLowPower() { + return this.chargedEnergyWithLowPower; + } + } + + private static enum DelayChargeResultState { + OK("OK - SoC as bevore"), // + WARNING("WARNING - Lower SoC"), // + PERFECT("PERFECT - Higher SoC"); + + private String text; + + DelayChargeResultState(String text) { + this.text = text; + } + } + @Test public void calculateAvailEnergy_test() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2020-01-01T08:00:00.00Z"), ZoneOffset.UTC); @@ -1634,11 +1666,11 @@ public void calculateAvailEnergy_test() throws Exception { final var sum = new DummySum(); final var predictorManager = new DummyPredictorManager( // Production - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_PRODUCTION_ACTIVE_POWER, midnight, DEFAULT_PRODUCTION_PREDICTION), SUM_PRODUCTION_ACTIVE_POWER), // Consumption - new DummyPredictor(PREDICTOR_ID, cm, + new DummyPredictor("predictor0", cm, Prediction.from(sum, SUM_CONSUMPTION_ACTIVE_POWER, midnight, DEFAULT_CONSUMPTION_PREDICTION), SUM_CONSUMPTION_ACTIVE_POWER)); diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/EnergyScheduleHandlerTest.java b/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/EnergyScheduleHandlerTest.java new file mode 100644 index 00000000000..0f33e93c960 --- /dev/null +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/EnergyScheduleHandlerTest.java @@ -0,0 +1,40 @@ +package io.openems.edge.controller.ess.gridoptimizedcharge; + +import static org.junit.Assert.assertEquals; + +import java.time.LocalTime; + +import org.apache.commons.math3.optim.nonlinear.scalar.GoalType; +import org.junit.Test; + +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.EnergyScheduleHandler.AbstractEnergyScheduleHandler; +import io.openems.edge.energy.api.simulation.Coefficient; +import io.openems.edge.energy.api.simulation.EnergyFlow; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; +import io.openems.edge.energy.api.simulation.OneSimulationContext; +import io.openems.edge.energy.api.test.DummyGlobalSimulationsContext; + +public class EnergyScheduleHandlerTest { + + @Test + public void testManual() { + var esh = ControllerEssGridOptimizedChargeImpl.buildEnergyScheduleHandler(// + () -> Mode.MANUAL, // + () -> LocalTime.of(10, 00)); + var gsc = DummyGlobalSimulationsContext.fromHandlers(esh); + ((AbstractEnergyScheduleHandler) esh /* this is safe */).initialize(gsc); + + assertEquals(3894, getEssMaxCharge(gsc, esh, 0)); + assertEquals(1214, getEssMaxCharge(gsc, esh, 26)); + assertEquals(4000, getEssMaxCharge(gsc, esh, 40)); + } + + private static int getEssMaxCharge(GlobalSimulationsContext gsc, EnergyScheduleHandler esh, int periodIndex) { + var osc = OneSimulationContext.from(gsc); + var period = gsc.periods().get(periodIndex); + var ef = EnergyFlow.Model.from(osc, period); + ((EnergyScheduleHandler.WithOnlyOneState) esh).simulatePeriod(OneSimulationContext.from(gsc), period, ef); + return ((int) ef.getExtremeCoefficientValue(Coefficient.ESS, GoalType.MINIMIZE)) * -1; + } +} diff --git a/io.openems.edge.controller.ess.hybrid.surplusfeedtogrid/test/io/openems/edge/controller/ess/hybrid/surplusfeedtogrid/ControllerEssHybridSurplusFeedToGridImplTest.java b/io.openems.edge.controller.ess.hybrid.surplusfeedtogrid/test/io/openems/edge/controller/ess/hybrid/surplusfeedtogrid/ControllerEssHybridSurplusFeedToGridImplTest.java index de89c02eda6..94d0e84fbb1 100644 --- a/io.openems.edge.controller.ess.hybrid.surplusfeedtogrid/test/io/openems/edge/controller/ess/hybrid/surplusfeedtogrid/ControllerEssHybridSurplusFeedToGridImplTest.java +++ b/io.openems.edge.controller.ess.hybrid.surplusfeedtogrid/test/io/openems/edge/controller/ess/hybrid/surplusfeedtogrid/ControllerEssHybridSurplusFeedToGridImplTest.java @@ -1,8 +1,10 @@ package io.openems.edge.controller.ess.hybrid.surplusfeedtogrid; +import static io.openems.edge.controller.ess.hybrid.surplusfeedtogrid.ControllerEssHybridSurplusFeedToGrid.ChannelId.SURPLUS_FEED_TO_GRID_IS_LIMITED; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_GREATER_OR_EQUALS; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.controller.test.ControllerTest; @@ -10,41 +12,33 @@ public class ControllerEssHybridSurplusFeedToGridImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final ChannelAddress CTRL_SURPLUS_FEED_TO_GRID_IS_LIMITED = new ChannelAddress(CTRL_ID, - "SurplusFeedToGridIsLimited"); - - private static final String ESS_ID = "ess0"; - - private static final ChannelAddress ESS_SET_ACTIVE_POWER_GREATER_OR_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerGreaterOrEquals"); - @Test public void test() throws Exception { - final var ess = new DummyHybridEss(ESS_ID); + final var ess = new DummyHybridEss("ess0"); final var test = new ControllerTest(new ControllerEssHybridSurplusFeedToGridImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("ess", ess) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .build()); ess.withSurplusPower(null); test.next(new TestCase() // - .output(ESS_SET_ACTIVE_POWER_GREATER_OR_EQUALS, null)); + .output("ess0", SET_ACTIVE_POWER_GREATER_OR_EQUALS, null)); ess.withSurplusPower(5000); ess.withMaxApparentPower(10000); test.next(new TestCase() // - .output(CTRL_SURPLUS_FEED_TO_GRID_IS_LIMITED, false) // - .output(ESS_SET_ACTIVE_POWER_GREATER_OR_EQUALS, 5000)); + .output(SURPLUS_FEED_TO_GRID_IS_LIMITED, false) // + .output("ess0", SET_ACTIVE_POWER_GREATER_OR_EQUALS, 5000)); ess.withSurplusPower(5000); ess.withMaxApparentPower(2000); test.next(new TestCase() // - .output(CTRL_SURPLUS_FEED_TO_GRID_IS_LIMITED, true) // - .output(ESS_SET_ACTIVE_POWER_GREATER_OR_EQUALS, 2000)); + .output(SURPLUS_FEED_TO_GRID_IS_LIMITED, true) // + .output("ess0", SET_ACTIVE_POWER_GREATER_OR_EQUALS, 2000)) // + + .deactivate(); } } \ No newline at end of file diff --git a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/ControllerEssLimiter14aImpl.java b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/ControllerEssLimiter14aImpl.java index a9c1082637f..f9ccb3b36ea 100644 --- a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/ControllerEssLimiter14aImpl.java +++ b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/ControllerEssLimiter14aImpl.java @@ -11,6 +11,7 @@ import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.Designate; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.channel.BooleanReadChannel; diff --git a/io.openems.edge.controller.ess.limiter14a/test/io/openems/edge/controller/ess/limiter14a/ControllerEssLimiter14aTest.java b/io.openems.edge.controller.ess.limiter14a/test/io/openems/edge/controller/ess/limiter14a/ControllerEssLimiter14aTest.java index 22405060dc7..b7d9cae4a52 100644 --- a/io.openems.edge.controller.ess.limiter14a/test/io/openems/edge/controller/ess/limiter14a/ControllerEssLimiter14aTest.java +++ b/io.openems.edge.controller.ess.limiter14a/test/io/openems/edge/controller/ess/limiter14a/ControllerEssLimiter14aTest.java @@ -1,61 +1,57 @@ package io.openems.edge.controller.ess.limiter14a; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_MODE; +import static io.openems.edge.controller.ess.limiter14a.ControllerEssLimiter14a.ChannelId.RESTRICTION_MODE; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_GREATER_OR_EQUALS; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; + import org.junit.Test; import io.openems.common.exceptions.OpenemsException; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.sum.GridMode; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; -import io.openems.edge.timedata.test.DummyTimedata; import io.openems.edge.io.test.DummyInputOutput; +import io.openems.edge.timedata.test.DummyTimedata; public class ControllerEssLimiter14aTest { - private static final String ESS_ID = "ess0"; - private static final String CTRL_ID = "ctrlEssLimiter14a0"; - - private static final ChannelAddress RESTRICTION_MODE = new ChannelAddress(CTRL_ID, "RestrictionMode"); - private static final ChannelAddress GPIO = new ChannelAddress("io0", "InputOutput0"); - private static final ChannelAddress LIMITATION = new ChannelAddress(ESS_ID, "SetActivePowerGreaterOrEquals"); - private static final ChannelAddress GRID_MODE = new ChannelAddress("_sum", "GridMode"); - @Test public void testController() throws OpenemsException, Exception { new ControllerTest(new ControllerEssLimiter14aImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // .addReference("timedata", new DummyTimedata("timedata0")) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .addReference("sum", new DummySum()) // .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID)// + .setId("ctrl0") // + .setEssId("ess0")// .setInputChannelAddress("io0/InputOutput0")// .build()) .next(new TestCase() // // Since logic is reversed - .input(GPIO, false) // - .input(GRID_MODE, GridMode.ON_GRID) - .output(LIMITATION, -4200) + .input("io0", INPUT_OUTPUT0, false) // + .input(GRID_MODE, GridMode.ON_GRID) // + .output("ess0", SET_ACTIVE_POWER_GREATER_OR_EQUALS, -4200) // .output(RESTRICTION_MODE, RestrictionMode.ON)) // .next(new TestCase() // - .input(GPIO, null) // - .output(LIMITATION, null)) // + .input("io0", INPUT_OUTPUT0, null) // + .output("ess0", SET_ACTIVE_POWER_GREATER_OR_EQUALS, null)) // .next(new TestCase() // - .input(GPIO, 1) // + .input("io0", INPUT_OUTPUT0, 1) // .input(GRID_MODE, GridMode.OFF_GRID) // .output(RESTRICTION_MODE, RestrictionMode.OFF)) // .next(new TestCase() // - .input(GPIO, false) // + .input("io0", INPUT_OUTPUT0, false) // .input(GRID_MODE, GridMode.OFF_GRID) // - .output(LIMITATION, null)) // - ; + .output("ess0", SET_ACTIVE_POWER_GREATER_OR_EQUALS, null)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.limittotaldischarge/bnd.bnd b/io.openems.edge.controller.ess.limittotaldischarge/bnd.bnd index 0f4d0e0a95c..db75c019243 100644 --- a/io.openems.edge.controller.ess.limittotaldischarge/bnd.bnd +++ b/io.openems.edge.controller.ess.limittotaldischarge/bnd.bnd @@ -8,7 +8,8 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.common,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ - io.openems.edge.ess.api + io.openems.edge.energy.api,\ + io.openems.edge.ess.api,\ -testpath: \ ${testpath} diff --git a/io.openems.edge.controller.ess.limittotaldischarge/src/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImpl.java b/io.openems.edge.controller.ess.limittotaldischarge/src/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImpl.java index a75ecf85f1b..e826546a526 100644 --- a/io.openems.edge.controller.ess.limittotaldischarge/src/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImpl.java +++ b/io.openems.edge.controller.ess.limittotaldischarge/src/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImpl.java @@ -1,8 +1,12 @@ package io.openems.edge.controller.ess.limittotaldischarge; +import static io.openems.edge.energy.api.EnergyUtils.socToEnergy; +import static java.lang.Math.max; + import java.time.Duration; import java.time.Instant; import java.util.Optional; +import java.util.function.IntSupplier; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -19,6 +23,8 @@ 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.energy.api.EnergySchedulable; +import io.openems.edge.energy.api.EnergyScheduleHandler; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.ess.power.api.Phase; import io.openems.edge.ess.power.api.Pwr; @@ -30,9 +36,10 @@ configurationPolicy = ConfigurationPolicy.REQUIRE // ) public class ControllerEssLimitTotalDischargeImpl extends AbstractOpenemsComponent - implements ControllerEssLimitTotalDischarge, Controller, OpenemsComponent { + implements ControllerEssLimitTotalDischarge, EnergySchedulable, Controller, OpenemsComponent { private final Logger log = LoggerFactory.getLogger(ControllerEssLimitTotalDischargeImpl.class); + private final EnergyScheduleHandler energyScheduleHandler; @Reference private ComponentManager componentManager; @@ -55,6 +62,8 @@ public ControllerEssLimitTotalDischargeImpl() { Controller.ChannelId.values(), // ControllerEssLimitTotalDischarge.ChannelId.values() // ); + this.energyScheduleHandler = buildEnergyScheduleHandler(// + () -> this.minSoc); } @Activate @@ -211,4 +220,27 @@ private boolean changeState(State nextState) { return false; } } + + /** + * Builds the {@link EnergyScheduleHandler}. + * + *

+ * This is public so that it can be used by the EnergyScheduler integration + * test. + * + * @param minSoc a supplier for the configured minSoc + * @return a {@link EnergyScheduleHandler} + */ + public static EnergyScheduleHandler buildEnergyScheduleHandler(IntSupplier minSoc) { + return EnergyScheduleHandler.of(// + simContext -> socToEnergy(simContext.ess().totalEnergy(), minSoc.getAsInt()), // + (simContext, period, energyFlow, minEnergy) -> { + energyFlow.setEssMaxDischarge(max(0, simContext.getEssInitial() - minEnergy)); + }); + } + + @Override + public EnergyScheduleHandler getEnergyScheduleHandler() { + return this.energyScheduleHandler; + } } diff --git a/io.openems.edge.controller.ess.limittotaldischarge/test/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImplTest.java b/io.openems.edge.controller.ess.limittotaldischarge/test/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImplTest.java index d86836ed672..f240abc3ed9 100644 --- a/io.openems.edge.controller.ess.limittotaldischarge/test/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImplTest.java +++ b/io.openems.edge.controller.ess.limittotaldischarge/test/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImplTest.java @@ -1,11 +1,14 @@ package io.openems.edge.controller.ess.limittotaldischarge; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischarge.ChannelId.AWAITING_HYSTERESIS; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_LESS_OR_EQUALS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.SOC; + import java.time.temporal.ChronoUnit; import org.junit.Test; -import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; @@ -13,47 +16,39 @@ public class ControllerEssLimitTotalDischargeImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final ChannelAddress CTRL_AWAITING_HYSTERESIS = new ChannelAddress(CTRL_ID, "AwaitingHysteresis"); - - private static final String ESS_ID = "ess0"; - private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerLessOrEquals"); - @Test public void test() throws Exception { - // Initialize mocked Clock - final var clock = new TimeLeapClock(); + final var clock = createDummyClock(); new ControllerTest(new ControllerEssLimitTotalDischargeImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID) // + .addComponent(new DummyManagedSymmetricEss("ess0") // .withSoc(20) // .withCapacity(9000)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setMinSoc(15) // .setForceChargeSoc(10) // .setForceChargePower(1000) // .build()) .next(new TestCase() // - .input(ESS_SOC, 20) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, null)// - .output(CTRL_AWAITING_HYSTERESIS, false)) // + .input("ess0", SOC, 20) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null)// + .output(AWAITING_HYSTERESIS, false)) // .next(new TestCase() // - .input(ESS_SOC, 15) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, 0) // - .output(CTRL_AWAITING_HYSTERESIS, false)) // + .input("ess0", SOC, 15) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 0) // + .output(AWAITING_HYSTERESIS, false)) // .next(new TestCase() // - .input(ESS_SOC, 16) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, 0) // - .output(CTRL_AWAITING_HYSTERESIS, true)) // + .input("ess0", SOC, 16) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 0) // + .output(AWAITING_HYSTERESIS, true)) // .next(new TestCase() // .timeleap(clock, 6, ChronoUnit.MINUTES) // - .input(ESS_SOC, 16) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, null) // - .output(CTRL_AWAITING_HYSTERESIS, false)); + .input("ess0", SOC, 16) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null) // + .output(AWAITING_HYSTERESIS, false)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.linearpowerband/test/io/openems/edge/controller/ess/linearpowerband/ControllerEssLinearPowerBandImplTest.java b/io.openems.edge.controller.ess.linearpowerband/test/io/openems/edge/controller/ess/linearpowerband/ControllerEssLinearPowerBandImplTest.java index 676304b3b6e..6e85c146ed7 100644 --- a/io.openems.edge.controller.ess.linearpowerband/test/io/openems/edge/controller/ess/linearpowerband/ControllerEssLinearPowerBandImplTest.java +++ b/io.openems.edge.controller.ess.linearpowerband/test/io/openems/edge/controller/ess/linearpowerband/ControllerEssLinearPowerBandImplTest.java @@ -1,8 +1,10 @@ package io.openems.edge.controller.ess.linearpowerband; +import static io.openems.edge.controller.ess.linearpowerband.ControllerEssLinearPowerBand.ChannelId.STATE_MACHINE; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.controller.test.ControllerTest; @@ -10,21 +12,14 @@ public class ControllerEssLinearPowerBandImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(CTRL_ID, "StateMachine"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - @Test public void test() throws Exception { new ControllerTest(new ControllerEssLinearPowerBandImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setMinPower(-1000) // .setMaxPower(1000) // .setAdjustPower(300) // @@ -32,49 +27,49 @@ public void test() throws Exception { .build()) // .next(new TestCase() // .output(STATE_MACHINE, State.DOWNWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -300)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -300)) // .next(new TestCase() // .output(STATE_MACHINE, State.DOWNWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -600)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -600)) // .next(new TestCase() // .output(STATE_MACHINE, State.DOWNWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -900)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -900)) // .next(new TestCase() // .output(STATE_MACHINE, State.DOWNWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -1000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -1000)) // .next(new TestCase() // .output(STATE_MACHINE, State.UPWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -700)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -700)) // .next(new TestCase() // .output(STATE_MACHINE, State.UPWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -400)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -400)) // .next(new TestCase() // .output(STATE_MACHINE, State.UPWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -100)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -100)) // .next(new TestCase() // .output(STATE_MACHINE, State.UPWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 200)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 200)) // .next(new TestCase() // .output(STATE_MACHINE, State.UPWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 500)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 500)) // .next(new TestCase() // .output(STATE_MACHINE, State.UPWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 800)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 800)) // .next(new TestCase() // .output(STATE_MACHINE, State.UPWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 1000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 1000)) // .next(new TestCase() // .output(STATE_MACHINE, State.DOWNWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 700)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 700)) // .next(new TestCase() // .output(STATE_MACHINE, State.DOWNWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 400)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 400)) // .next(new TestCase() // .output(STATE_MACHINE, State.DOWNWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 100)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 100)) // .next(new TestCase() // .output(STATE_MACHINE, State.DOWNWARDS) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -200)) // - ; + .output("ess0", SET_ACTIVE_POWER_EQUALS, -200)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.mindischargeperiod/test/io/openems/edge/controller/ess/mindischargeperiod/ControllerEssMinimumDischargePowerImplTest.java b/io.openems.edge.controller.ess.mindischargeperiod/test/io/openems/edge/controller/ess/mindischargeperiod/ControllerEssMinimumDischargePowerImplTest.java index e2a90e1983b..53afa7614c7 100644 --- a/io.openems.edge.controller.ess.mindischargeperiod/test/io/openems/edge/controller/ess/mindischargeperiod/ControllerEssMinimumDischargePowerImplTest.java +++ b/io.openems.edge.controller.ess.mindischargeperiod/test/io/openems/edge/controller/ess/mindischargeperiod/ControllerEssMinimumDischargePowerImplTest.java @@ -7,21 +7,18 @@ public class ControllerEssMinimumDischargePowerImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerEssMinimumDischargePowerImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setActivateDischargePower(0) // .setDischargeTime(0) // .setMinDischargePower(0) // - .build()); // - ; + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ControllerEssReactivePowerVoltageCharacteristicImplTest.java b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ControllerEssReactivePowerVoltageCharacteristicImplTest.java index 1b4492ede55..442be9ddfa2 100644 --- a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ControllerEssReactivePowerVoltageCharacteristicImplTest.java +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ControllerEssReactivePowerVoltageCharacteristicImplTest.java @@ -1,14 +1,15 @@ package io.openems.edge.controller.ess.reactivepowervoltagecharacteristic; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; +import static io.openems.common.utils.JsonUtils.buildJsonArray; +import static io.openems.common.utils.JsonUtils.buildJsonObject; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_REACTIVE_POWER_EQUALS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.MAX_APPARENT_POWER; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.VOLTAGE; +import static java.time.temporal.ChronoUnit.SECONDS; import org.junit.Test; -import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; -import io.openems.common.utils.JsonUtils; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -18,86 +19,79 @@ public class ControllerEssReactivePowerVoltageCharacteristicImplTest { - private static final String CTRL_ID = "ctrlReactivePowerVoltageCharacteristic0"; - private static final String ESS_ID = "ess0"; - private static final String METER_ID = "meter0"; - private static final ChannelAddress ESS_REACTIVE_POWER = new ChannelAddress(ESS_ID, "SetReactivePowerEquals"); - private static final ChannelAddress METER_VOLTAGE = new ChannelAddress(METER_ID, "Voltage"); - private static final ChannelAddress MAX_APPARENT_POWER = new ChannelAddress(ESS_ID, "MaxApparentPower"); - @Test public void test() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-10-05T14:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); new ControllerTest(new ControllerEssReactivePowerVoltageCharacteristicImpl())// .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("meter", new DummyElectricityMeter(METER_ID)) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("meter", new DummyElectricityMeter("meter0")) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create()// - .setId(CTRL_ID)// - .setEssId(ESS_ID)// - .setMeterId(METER_ID)// + .setId("ctrl0")// + .setEssId("ess0")// + .setMeterId("meter0")// .setNominalVoltage(240)// .setWaitForHysteresis(5)// - .setPowerVoltConfig(JsonUtils.buildJsonArray()// - .add(JsonUtils.buildJsonObject()// + .setPowerVoltConfig(buildJsonArray()// + .add(buildJsonObject()// .addProperty("voltageRatio", 0.9) // .addProperty("percent", 60) // .build()) // - .add(JsonUtils.buildJsonObject()// + .add(buildJsonObject()// .addProperty("voltageRatio", 0.93) // .addProperty("percent", 0) // .build()) // - .add(JsonUtils.buildJsonObject()// + .add(buildJsonObject()// .addProperty("voltageRatio", 1.07) // .addProperty("percent", 0) // .build()) // - .add(JsonUtils.buildJsonObject()// + .add(buildJsonObject()// .addProperty("voltageRatio", 1.1) // .addProperty("percent", -60) // .build() // ).build().toString() // ).build()) // .next(new TestCase("First Input") // - .input(METER_VOLTAGE, 240_000) // [mV] - .input(MAX_APPARENT_POWER, 10_000)) // [VA] + .input("meter0", VOLTAGE, 240_000) // [mV] + .input("ess0", MAX_APPARENT_POWER, 10_000)) // [VA] .next(new TestCase() // - .output(ESS_REACTIVE_POWER, 0))// + .output("ess0", SET_REACTIVE_POWER_EQUALS, 0))// .next(new TestCase("Second Input") // - .timeleap(clock, 5, ChronoUnit.SECONDS)// - .input(METER_VOLTAGE, 216_000) // [mV] - .input(MAX_APPARENT_POWER, 10_000) // [VA] - .output(ESS_REACTIVE_POWER, 6000))// + .timeleap(clock, 5, SECONDS)// + .input("meter0", VOLTAGE, 216_000) // [mV] + .input("ess0", MAX_APPARENT_POWER, 10_000) // [VA] + .output("ess0", SET_REACTIVE_POWER_EQUALS, 6000))// .next(new TestCase("Third Input")// - .timeleap(clock, 5, ChronoUnit.SECONDS)// - .input(METER_VOLTAGE, 220_000) // [mV] - .input(MAX_APPARENT_POWER, 10_000) // [VA] - .output(ESS_REACTIVE_POWER, 2600))// + .timeleap(clock, 5, SECONDS)// + .input("meter0", VOLTAGE, 220_000) // [mV] + .input("ess0", MAX_APPARENT_POWER, 10_000) // [VA] + .output("ess0", SET_REACTIVE_POWER_EQUALS, 2600))// .next(new TestCase()// - .timeleap(clock, 5, ChronoUnit.SECONDS)// - .input(METER_VOLTAGE, 223_000) // [mV] - .input(MAX_APPARENT_POWER, 10_000) // [VA] - .output(ESS_REACTIVE_POWER, 100))// + .timeleap(clock, 5, SECONDS)// + .input("meter0", VOLTAGE, 223_000) // [mV] + .input("ess0", MAX_APPARENT_POWER, 10_000) // [VA] + .output("ess0", SET_REACTIVE_POWER_EQUALS, 100))// .next(new TestCase()// - .timeleap(clock, 5, ChronoUnit.SECONDS)// - .input(METER_VOLTAGE, 223_200) // [mV] - .input(MAX_APPARENT_POWER, 10_000) // [VA] - .output(ESS_REACTIVE_POWER, 0))// + .timeleap(clock, 5, SECONDS)// + .input("meter0", VOLTAGE, 223_200) // [mV] + .input("ess0", MAX_APPARENT_POWER, 10_000) // [VA] + .output("ess0", SET_REACTIVE_POWER_EQUALS, 0))// .next(new TestCase()// - .timeleap(clock, 5, ChronoUnit.SECONDS)// - .input(METER_VOLTAGE, 256_800) // [mV] - .input(MAX_APPARENT_POWER, 10_000) // [VA] - .output(ESS_REACTIVE_POWER, 0))// + .timeleap(clock, 5, SECONDS)// + .input("meter0", VOLTAGE, 256_800) // [mV] + .input("ess0", MAX_APPARENT_POWER, 10_000) // [VA] + .output("ess0", SET_REACTIVE_POWER_EQUALS, 0))// .next(new TestCase()// - .timeleap(clock, 5, ChronoUnit.SECONDS)// - .input(METER_VOLTAGE, 260_000) // [mV] - .input(MAX_APPARENT_POWER, 10_000) // [VA] - .output(ESS_REACTIVE_POWER, -2600))// + .timeleap(clock, 5, SECONDS)// + .input("meter0", VOLTAGE, 260_000) // [mV] + .input("ess0", MAX_APPARENT_POWER, 10_000) // [VA] + .output("ess0", SET_REACTIVE_POWER_EQUALS, -2600))// .next(new TestCase()// - .timeleap(clock, 5, ChronoUnit.SECONDS)// - .input(METER_VOLTAGE, 264_000) // [mV] - .input(MAX_APPARENT_POWER, 10_000) // [VA] - .output(ESS_REACTIVE_POWER, -6000))// - ; + .timeleap(clock, 5, SECONDS)// + .input("meter0", VOLTAGE, 264_000) // [mV] + .input("ess0", MAX_APPARENT_POWER, 10_000) // [VA] + .output("ess0", SET_REACTIVE_POWER_EQUALS, -6000)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.ess.selltogridlimit/test/io/openems/edge/controller/symmetric/selltogridlimit/ControllerEssSellToGridLimitImplTest.java b/io.openems.edge.controller.ess.selltogridlimit/test/io/openems/edge/controller/symmetric/selltogridlimit/ControllerEssSellToGridLimitImplTest.java index 5c191d0c4f5..0870799ee2a 100644 --- a/io.openems.edge.controller.ess.selltogridlimit/test/io/openems/edge/controller/symmetric/selltogridlimit/ControllerEssSellToGridLimitImplTest.java +++ b/io.openems.edge.controller.ess.selltogridlimit/test/io/openems/edge/controller/symmetric/selltogridlimit/ControllerEssSellToGridLimitImplTest.java @@ -1,47 +1,39 @@ package io.openems.edge.controller.symmetric.selltogridlimit; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_LESS_OR_EQUALS; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.test.DummyElectricityMeter; public class ControllerEssSellToGridLimitImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String ESS_ID = "ess0"; - - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerLessOrEquals"); - - private static final String METER_ID = "meter00"; - - private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - @Test public void test() throws Exception { new ControllerTest(new ControllerEssSellToGridLimitImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // - .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // + .addReference("meter", new DummyElectricityMeter("meter0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .setId("ctrl0") // + .setEssId("ess0") // + .setMeterId("meter0") // .setMaximumSellToGridPower(5000) // .build()) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -5000) // - .input(ESS_ACTIVE_POWER, 3000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, null)) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -5000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 3000) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, null)) // .next(new TestCase() // - .input(METER_ACTIVE_POWER, -6000) // - .input(ESS_ACTIVE_POWER, 3000) // - .output(ESS_SET_ACTIVE_POWER_LESS_OR_EQUALS, 2000)); + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -6000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 3000) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 2000)) // + .deactivate(); } } \ No newline at end of file diff --git a/io.openems.edge.controller.ess.standby/test/io/openems/edge/controller/ess/standby/ControllerEssStandbyImplTest.java b/io.openems.edge.controller.ess.standby/test/io/openems/edge/controller/ess/standby/ControllerEssStandbyImplTest.java index 6f3053a0b16..ac45a6cbb7e 100644 --- a/io.openems.edge.controller.ess.standby/test/io/openems/edge/controller/ess/standby/ControllerEssStandbyImplTest.java +++ b/io.openems.edge.controller.ess.standby/test/io/openems/edge/controller/ess/standby/ControllerEssStandbyImplTest.java @@ -1,5 +1,15 @@ package io.openems.edge.controller.ess.standby; +import static io.openems.edge.common.sum.Sum.ChannelId.CONSUMPTION_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.PRODUCTION_ACTIVE_POWER; +import static io.openems.edge.controller.ess.standby.ControllerEssStandby.ChannelId.STATE_MACHINE; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.ALLOWED_CHARGE_POWER; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.ACTIVE_POWER; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MINUTES; + import java.time.DayOfWeek; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -9,10 +19,8 @@ import org.junit.Test; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.sum.GridMode; -import io.openems.edge.common.sum.Sum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.ess.standby.statemachine.StateMachine.State; @@ -23,23 +31,6 @@ public class ControllerEssStandbyImplTest { private static final int MAX_APPARENT_POWER = 50_000; // [W] - private static final String CTRL_ID = "ctrlEssStandby0"; - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(CTRL_ID, "StateMachine"); - - private static final String SUM_ID = Sum.SINGLETON_COMPONENT_ID; - private static final ChannelAddress SUM_GRID_ACTIVE_POWER = new ChannelAddress(SUM_ID, "GridActivePower"); - private static final ChannelAddress SUM_PRODUCTION_ACTIVE_POWER = new ChannelAddress(SUM_ID, - "ProductionActivePower"); - private static final ChannelAddress SUM_CONSUMPTION_ACTIVE_POWER = new ChannelAddress(SUM_ID, - "ConsumptionActivePower"); - - private static final String ESS_ID = "ess0"; - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - private static final ChannelAddress ESS_ALLOWED_CHARGE_POWER = new ChannelAddress(ESS_ID, "AllowedChargePower"); - - // Initialize mocked Clock private static TimeLeapClock clock; @Before @@ -49,7 +40,7 @@ public void initialize() { private static ControllerTest tillDischarge() throws Exception { // Initialize ESS - final var ess = new DummyManagedSymmetricEss(ESS_ID) // + final var ess = new DummyManagedSymmetricEss("ess0") // .withGridMode(GridMode.ON_GRID) // .withMaxApparentPower(MAX_APPARENT_POWER) // .withSoc(70); @@ -59,8 +50,8 @@ private static ControllerTest tillDischarge() throws Exception { .addReference("sum", new DummySum()) // .addComponent(ess) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setStartDate("01.02.2020") // .setEndDate("01.03.2020") // .setDayOfWeek(DayOfWeek.SUNDAY) // @@ -68,7 +59,7 @@ private static ControllerTest tillDischarge() throws Exception { .next(new TestCase() // .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase("1 second before midnight (friday) before 01.02.2020") // - .timeleap(clock, 10, ChronoUnit.MINUTES) // + .timeleap(clock, 10, MINUTES) // .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase("midnight (saturday)") // .timeleap(clock, 1, ChronoUnit.SECONDS) // @@ -80,113 +71,117 @@ private static ControllerTest tillDischarge() throws Exception { * DISCHARGE */ .next(new TestCase("sunday -> switch to DISCHARGE") // - .input(SUM_GRID_ACTIVE_POWER, 10_000 /* buy from grid */) // - .input(ESS_ACTIVE_POWER, 100 /* discharge */) // + .input(GRID_ACTIVE_POWER, 10_000 /* buy from grid */) // + .input("ess0", ACTIVE_POWER, 100 /* discharge */) // .output(STATE_MACHINE, State.DISCHARGE) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 10_100)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 10_100)) // .next(new TestCase("discharge > 70 % of maxApparentPower") // - .timeleap(clock, 30, ChronoUnit.MINUTES) // - .input(SUM_GRID_ACTIVE_POWER, 29_900 /* buy from grid */) // - .input(ESS_ACTIVE_POWER, 10_100 /* discharge */) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 40_000)) // + .timeleap(clock, 30, MINUTES) // + .input(GRID_ACTIVE_POWER, 29_900 /* buy from grid */) // + .input("ess0", ACTIVE_POWER, 10_100 /* discharge */) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 40_000)) // .next(new TestCase("discharge > 70 % of maxApparentPower - 9 minutes") // - .timeleap(clock, 9, ChronoUnit.MINUTES) // - .input(SUM_GRID_ACTIVE_POWER, 0 /* buy from grid */) // - .input(ESS_ACTIVE_POWER, 40_000 /* discharge */) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 40_000)) // + .timeleap(clock, 9, MINUTES) // + .input(GRID_ACTIVE_POWER, 0 /* buy from grid */) // + .input("ess0", ACTIVE_POWER, 40_000 /* discharge */) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 40_000)) // .next(new TestCase("discharge > 70 % of maxApparentPower - 10 minutes: reduce to 50 %") // - .timeleap(clock, 1, ChronoUnit.MINUTES) // - .input(SUM_GRID_ACTIVE_POWER, 0 /* buy from grid */) // - .input(ESS_ACTIVE_POWER, 40_000 /* discharge */) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, (int) (MAX_APPARENT_POWER * 0.5))) + .timeleap(clock, 1, MINUTES) // + .input(GRID_ACTIVE_POWER, 0 /* buy from grid */) // + .input("ess0", ACTIVE_POWER, 40_000 /* discharge */) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, (int) (MAX_APPARENT_POWER * 0.5))) .next(new TestCase("do not charge") // - .timeleap(clock, 1, ChronoUnit.MINUTES) // - .input(SUM_GRID_ACTIVE_POWER, -100 /* buy from grid */) // - .input(ESS_ACTIVE_POWER, 0 /* discharge */) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 0)); + .timeleap(clock, 1, MINUTES) // + .input(GRID_ACTIVE_POWER, -100 /* buy from grid */) // + .input("ess0", ACTIVE_POWER, 0 /* discharge */) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 0)) // + .deactivate(); } private static ControllerTest tillSlowCharge1_1() throws Exception { return tillDischarge() // .next(new TestCase("production > consumption") // - .input(SUM_GRID_ACTIVE_POWER, 0 /* buy from grid */) // - .input(SUM_PRODUCTION_ACTIVE_POWER, 1000) // - .input(SUM_CONSUMPTION_ACTIVE_POWER, 999) // - .input(ESS_ACTIVE_POWER, 10_000 /* discharge */) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 10_000)) // + .input(GRID_ACTIVE_POWER, 0 /* buy from grid */) // + .input(PRODUCTION_ACTIVE_POWER, 1000) // + .input(CONSUMPTION_ACTIVE_POWER, 999) // + .input("ess0", ACTIVE_POWER, 10_000 /* discharge */) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 10_000)) // .next(new TestCase("production > consumption: more than 1 minute -> SLOW_CHARGE") // - .timeleap(clock, 1, ChronoUnit.MINUTES) // - .input(SUM_GRID_ACTIVE_POWER, 0 /* buy from grid */) // - .input(SUM_PRODUCTION_ACTIVE_POWER, 1000) // - .input(SUM_CONSUMPTION_ACTIVE_POWER, 999)) // + .timeleap(clock, 1, MINUTES) // + .input(GRID_ACTIVE_POWER, 0 /* buy from grid */) // + .input(PRODUCTION_ACTIVE_POWER, 1000) // + .input(CONSUMPTION_ACTIVE_POWER, 999)) // /* * SLOW_CHARGE */ .next(new TestCase("SLOW_CHARGE") // - .output(STATE_MACHINE, State.SLOW_CHARGE_1)); // + .output(STATE_MACHINE, State.SLOW_CHARGE_1)) // + .deactivate(); } private static ControllerTest tillSlowCharge1_2() throws Exception { return tillDischarge() // .next(new TestCase("latest at 12 -> SLOW_CHARGE") // - .timeleap(clock, 12, ChronoUnit.HOURS)) // + .timeleap(clock, 12, HOURS)) // /* * SLOW_CHARGE */ .next(new TestCase("SLOW_CHARGE") // - .output(STATE_MACHINE, State.SLOW_CHARGE_1)); // + .output(STATE_MACHINE, State.SLOW_CHARGE_1)) // + .deactivate(); } private static ControllerTest tillSlowCharge2_1() throws Exception { return tillSlowCharge1_2() // .next(new TestCase("") // - .input(ESS_ACTIVE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, -20_000 /* sell to grid */) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -20_000)) // + .input("ess0", ACTIVE_POWER, 0) // + .input(GRID_ACTIVE_POWER, -20_000 /* sell to grid */) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -20_000)) // .next(new TestCase("Charge with minimum 20 %") // - .input(ESS_ACTIVE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, (int) (MAX_APPARENT_POWER * -0.19) /* sell to grid */) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, (int) (MAX_APPARENT_POWER * -0.20))) // + .input("ess0", ACTIVE_POWER, 0) // + .input(GRID_ACTIVE_POWER, (int) (MAX_APPARENT_POWER * -0.19)) // sell to grid + .output("ess0", SET_ACTIVE_POWER_EQUALS, (int) (MAX_APPARENT_POWER * -0.20))) // .next(new TestCase("Charge with maximum 50 %") // - .input(ESS_ACTIVE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, (int) (MAX_APPARENT_POWER * -0.51) /* sell to grid */) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, (int) (MAX_APPARENT_POWER * -0.50))) // + .input("ess0", ACTIVE_POWER, 0) // + .input(GRID_ACTIVE_POWER, (int) (MAX_APPARENT_POWER * -0.51)) // sell to grid + .output("ess0", SET_ACTIVE_POWER_EQUALS, (int) (MAX_APPARENT_POWER * -0.50))) // .next(new TestCase("after 30 minutes -> FAST_CHARGE") // - .timeleap(clock, 30, ChronoUnit.MINUTES)) // + .timeleap(clock, 30, MINUTES)) // /* * FAST_CHARGE */ .next(new TestCase("FAST_CHARGE with max power") // .output(STATE_MACHINE, State.FAST_CHARGE) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, MAX_APPARENT_POWER * -1)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, MAX_APPARENT_POWER * -1)) // .next(new TestCase("after 10 minutes -> SLOW_CHARGE_2") // - .timeleap(clock, 10, ChronoUnit.MINUTES)) // + .timeleap(clock, 10, MINUTES)) // /* * SLOW_CHARGE_2 */ .next(new TestCase("SLOW_CHARGE_2") // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_ALLOWED_CHARGE_POWER, -60_000) // - .input(SUM_GRID_ACTIVE_POWER, -20_000 /* sell to grid */) // + .input("ess0", ACTIVE_POWER, 0) // + .input("ess0", ALLOWED_CHARGE_POWER, -60_000) // + .input(GRID_ACTIVE_POWER, -20_000 /* sell to grid */) // .output(STATE_MACHINE, State.SLOW_CHARGE_2) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -20_000)) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -20_000)) // /* * FINISHED */ .next(new TestCase("no more charging allowed -> FINISHED") // - .input(ESS_ALLOWED_CHARGE_POWER, 0)) // + .input("ess0", ALLOWED_CHARGE_POWER, 0)) // .next(new TestCase("FINISHED") // .output(STATE_MACHINE, State.FINISHED)) // /* * UNDEFINED */ .next(new TestCase("on day change -> UNDEFINED") // - .timeleap(clock, 11, ChronoUnit.HOURS)) // + .timeleap(clock, 11, HOURS)) // .next(new TestCase("UNDEFINED") // .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase("After test period") // .timeleap(clock, 30, ChronoUnit.DAYS) // - .output(STATE_MACHINE, State.UNDEFINED)); // + .output(STATE_MACHINE, State.UNDEFINED)) // + .deactivate(); } @Test diff --git a/io.openems.edge.controller.ess.timeofusetariff/bnd.bnd b/io.openems.edge.controller.ess.timeofusetariff/bnd.bnd index d45bd8553ce..767f72aac39 100644 --- a/io.openems.edge.controller.ess.timeofusetariff/bnd.bnd +++ b/io.openems.edge.controller.ess.timeofusetariff/bnd.bnd @@ -3,6 +3,8 @@ Bundle-Vendor: FENECON GmbH Bundle-License: https://opensource.org/licenses/EPL-2.0 Bundle-Version: 1.0.0.${tstamp} +# TODO remove emergencycapacityreserve and limittotaldischarge after v1 + -buildpath: \ ${buildpath},\ io.openems.common,\ @@ -14,6 +16,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.ess.api,\ io.openems.edge.timedata.api,\ io.openems.edge.timeofusetariff.api,\ + org.apache.commons.math3,\ -testpath: \ ${testpath},\ diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Config.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Config.java index 99793e89191..bceba3c4d55 100644 --- a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Config.java +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Config.java @@ -3,6 +3,8 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import io.openems.edge.energy.api.Version; + @ObjectClassDefinition(// name = "Controller Ess Time-Of-Use Tariff", // description = "Optimize behaviour of an ESS in combination with a Time-Of-Use (ToU) Tariff.") @@ -40,9 +42,12 @@ @AttributeDefinition(name = "Limit Charge Power for §14a EnWG", description = "Always apply §14a EnWG limitation of 4.2 kW") boolean limitChargePowerFor14aEnWG() default false; + @AttributeDefinition(name = "Version", description = "Select version of implementation") + Version version() default Version.V1_ESS_ONLY; + @AttributeDefinition(name = "Ess target filter", description = "This is auto-generated by 'Ess-ID'.") String ess_target() default "(enabled=true)"; String webconsole_configurationFactory_nameHint() default "Controller Ess Time-Of-Use Tariff [{id}]"; -} \ No newline at end of file +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffController.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffController.java index 9876f79201e..b241b53977a 100644 --- a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffController.java +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffController.java @@ -8,10 +8,11 @@ import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.ess.timeofusetariff.v1.EnergyScheduleHandlerV1; +import io.openems.edge.energy.api.EnergySchedulable; -public interface TimeOfUseTariffController extends Controller, OpenemsComponent { - - public static final int PERIODS_PER_HOUR = 4; +@SuppressWarnings("deprecation") +public interface TimeOfUseTariffController extends Controller, EnergySchedulable, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { /** @@ -52,6 +53,14 @@ public Doc doc() { } } + /** + * Get the {@link EnergyScheduleHandlerV1}. + * + * @return {@link EnergyScheduleHandlerV1} + */ + @Deprecated + public EnergyScheduleHandlerV1 getEnergyScheduleHandlerV1(); + /** * Gets the Channel for {@link ChannelId#QUARTERLY_PRICES}. * diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImpl.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImpl.java index 5ae75684899..48b0bc09338 100644 --- a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImpl.java +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImpl.java @@ -1,12 +1,25 @@ package io.openems.edge.controller.ess.timeofusetariff; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.CHARGE_GRID; import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.ESS_MAX_SOC; import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateAutomaticMode; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateChargeEnergyInChargeGrid; +import static io.openems.edge.energy.api.simulation.Coefficient.ESS; +import static io.openems.edge.energy.api.simulation.Coefficient.GRID_TO_CONS; +import static io.openems.edge.energy.api.simulation.Coefficient.GRID_TO_ESS; +import static io.openems.edge.energy.api.simulation.Coefficient.PROD_TO_ESS; +import static io.openems.edge.energy.api.simulation.Coefficient.PROD_TO_GRID; +import static java.lang.Math.min; +import static java.lang.Math.round; +import static org.apache.commons.math3.optim.linear.Relationship.EQ; +import static org.apache.commons.math3.optim.nonlinear.scalar.GoalType.MAXIMIZE; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BooleanSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -31,11 +44,16 @@ import io.openems.edge.controller.api.Controller; import io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve; import io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischarge; -import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.Context; import io.openems.edge.controller.ess.timeofusetariff.Utils.ApplyState; +import io.openems.edge.controller.ess.timeofusetariff.jsonrpc.GetScheduleRequest; +import io.openems.edge.controller.ess.timeofusetariff.jsonrpc.GetScheduleResponse; +import io.openems.edge.controller.ess.timeofusetariff.v1.EnergyScheduleHandlerV1; +import io.openems.edge.controller.ess.timeofusetariff.v1.EnergyScheduleHandlerV1.ContextV1; +import io.openems.edge.controller.ess.timeofusetariff.v1.UtilsV1; import io.openems.edge.energy.api.EnergySchedulable; import io.openems.edge.energy.api.EnergyScheduleHandler; import io.openems.edge.energy.api.EnergyScheduler; +import io.openems.edge.energy.api.simulation.EnergyFlow; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.ess.power.api.Phase; import io.openems.edge.ess.power.api.Pwr; @@ -50,15 +68,13 @@ immediate = true, // configurationPolicy = ConfigurationPolicy.REQUIRE // ) +@SuppressWarnings("deprecation") public class TimeOfUseTariffControllerImpl extends AbstractOpenemsComponent implements TimeOfUseTariffController, - EnergySchedulable, Controller, OpenemsComponent, TimedataProvider, ComponentJsonApi { + EnergySchedulable, Controller, OpenemsComponent, TimedataProvider, ComponentJsonApi { - public static record Context(List ctrlEmergencyCapacityReserves, - List ctrlLimitTotalDischarges, ManagedSymmetricEss ess, - ControlMode controlMode, int maxChargePowerFromGrid, boolean limitChargePowerFor14aEnWG) { - } - - private final EnergyScheduleHandler energyScheduleHandler; + @Deprecated + private final EnergyScheduleHandlerV1 energyScheduleHandlerV1; + private final EnergyScheduleHandler.WithDifferentStates energyScheduleHandler; private final CalculateActiveTime calculateDelayedTime = new CalculateActiveTime(this, TimeOfUseTariffController.ChannelId.DELAYED_TIME); private final CalculateActiveTime calculateChargedTime = new CalculateActiveTime(this, @@ -80,20 +96,23 @@ public static record Context(List ctrlEme @Reference(policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) private volatile Timedata timedata; + @Deprecated @Reference(policyOption = ReferencePolicyOption.GREEDY, // cardinality = ReferenceCardinality.MULTIPLE, // target = "(&(enabled=true)(isReserveSocEnabled=true))") - private List ctrlEmergencyCapacityReserves = new CopyOnWriteArrayList<>(); + private volatile List ctrlEmergencyCapacityReserves = new CopyOnWriteArrayList<>(); + @Deprecated @Reference(policyOption = ReferencePolicyOption.GREEDY, // cardinality = ReferenceCardinality.MULTIPLE, // target = "(enabled=true)") - private List ctrlLimitTotalDischarges = new CopyOnWriteArrayList<>(); + private volatile List ctrlLimitTotalDischarges = new CopyOnWriteArrayList<>(); @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) private ManagedSymmetricEss ess; @Reference + @Deprecated private EnergyScheduler energyScheduler; private Config config = null; @@ -104,11 +123,18 @@ public TimeOfUseTariffControllerImpl() { Controller.ChannelId.values(), // TimeOfUseTariffController.ChannelId.values() // ); - this.energyScheduleHandler = new EnergyScheduleHandler<>(// + + this.energyScheduleHandlerV1 = new EnergyScheduleHandlerV1(// () -> this.config.controlMode().states, // - () -> new Context(this.ctrlEmergencyCapacityReserves, this.ctrlLimitTotalDischarges, this.ess, + () -> new ContextV1(this.ctrlEmergencyCapacityReserves, this.ctrlLimitTotalDischarges, this.ess, this.config.controlMode(), this.config.maxChargePowerFromGrid(), this.config.limitChargePowerFor14aEnWG())); + + this.energyScheduleHandler = buildEnergyScheduleHandler(// + () -> this.ess, // + () -> this.config.controlMode(), // + () -> this.config.maxChargePowerFromGrid(), // + () -> this.config.limitChargePowerFor14aEnWG()); } @Activate @@ -121,6 +147,7 @@ private void activate(ComponentContext context, Config config) { private void modified(ComponentContext context, Config config) { super.modified(context, config.id(), config.alias(), config.enabled()); this.applyConfig(config); + this.energyScheduleHandler.triggerReschedule(); } private synchronized void applyConfig(Config config) { @@ -138,12 +165,26 @@ protected void deactivate() { @Override public void run() throws OpenemsNamedException { - // Mode given from the configuration. - var as = switch (this.config.mode()) { - case AUTOMATIC -> calculateAutomaticMode(this.sum, this.ess, - this.energyScheduleHandler.getCurrentEssChargeInChargeGrid(), this.config.maxChargePowerFromGrid(), - this.config.limitChargePowerFor14aEnWG(), this.getCurrentPeriodState()); - case OFF -> new ApplyState(StateMachine.BALANCING, null); + // Version and Mode given from the configuration. + final var as = switch (this.config.version()) { + + case V1_ESS_ONLY // + -> switch (this.config.mode()) { + case AUTOMATIC // + -> UtilsV1.calculateAutomaticMode(this.energyScheduleHandlerV1, this.sum, this.ess, + this.config.maxChargePowerFromGrid(), this.config.limitChargePowerFor14aEnWG()); + case OFF // + -> new ApplyState(StateMachine.BALANCING, null); + }; + + case V2_ENERGY_SCHEDULABLE // + -> switch (this.config.mode()) { + case AUTOMATIC // + -> calculateAutomaticMode(this.sum, this.ess, this.config.maxChargePowerFromGrid(), + this.config.limitChargePowerFor14aEnWG(), this.energyScheduleHandler.getCurrentPeriod()); + case OFF // + -> new ApplyState(StateMachine.BALANCING, null); + }; }; // Update Channels @@ -159,14 +200,6 @@ public void run() throws OpenemsNamedException { } } - private StateMachine getCurrentPeriodState() { - var state = this.energyScheduleHandler.getCurrentState(); - if (state != null) { - return state; - } - return BALANCING; // Default Fallback - } - @Override public Timedata getTimedata() { return this.timedata; @@ -174,21 +207,134 @@ public Timedata getTimedata() { @Override public void buildJsonApiRoutes(JsonApiBuilder builder) { - this.energyScheduler.buildJsonApiRoutes(builder); + builder.handleRequest(GetScheduleRequest.METHOD, call -> // + switch (this.config.version()) { + case V1_ESS_ONLY // + -> this.energyScheduler.handleGetScheduleRequestV1(call, this.id()); + + case V2_ENERGY_SCHEDULABLE // + -> GetScheduleResponse.from(call.getRequest().getId(), // + this.id(), this.componentManager.getClock(), this.ess, this.timedata, this.energyScheduleHandler); + }); } @Override public String debugLog() { var b = new StringBuilder() // .append(this.getStateMachine()); // - if (this.getCurrentPeriodState() == null) { - b.append("|No Schedule available"); + + switch (this.config.version()) { + case V1_ESS_ONLY -> { + if (this.energyScheduleHandlerV1 == null || this.energyScheduleHandlerV1.getCurrentState() == null) { + b.append("|No Schedule available"); + } + } + case V2_ENERGY_SCHEDULABLE -> { + if (this.energyScheduleHandler.getCurrentPeriod() == null) { + b.append("|No Schedule available"); + } + } } return b.toString(); } + /** + * Builds the {@link EnergyScheduleHandler}. + * + *

+ * This is public so that it can be used by the EnergyScheduler integration + * test. + * + * @param ess a supplier for the + * {@link ManagedSymmetricEss} + * @param controlMode a supplier for the configured + * {@link ControlMode} + * @param maxChargePowerFromGrid a supplier for the configured + * maxChargePowerFromGrid + * @param limitChargePowerFor14aEnWG a supplier for the configured + * limitChargePowerFor14aEnWG + * @return a typed {@link EnergyScheduleHandler} + */ + public static EnergyScheduleHandler.WithDifferentStates buildEnergyScheduleHandler( + Supplier ess, Supplier controlMode, IntSupplier maxChargePowerFromGrid, + BooleanSupplier limitChargePowerFor14aEnWG) { + return EnergyScheduleHandler.of(// + StateMachine.BALANCING, // + () -> controlMode.get().states, // + simContext -> { + // Maximium-SoC in CHARGE_GRID is 90 % + var maxSocEnergyInChargeGrid = round(simContext.ess().totalEnergy() * (ESS_MAX_SOC / 100)); + var essChargeInChargeGrid = calculateChargeEnergyInChargeGrid(simContext); + return new EshContext(ess.get(), controlMode.get(), maxChargePowerFromGrid.getAsInt(), + limitChargePowerFor14aEnWG.getAsBoolean(), maxSocEnergyInChargeGrid, essChargeInChargeGrid); + }, // + (simContext, period, energyFlow, ctrlContext, state) -> { + switch (state) { + case BALANCING -> applyBalancing(energyFlow); // TODO Move to CtrlBalancing + case DELAY_DISCHARGE -> applyDelayDischarge(energyFlow); + case CHARGE_GRID -> { + energyFlow.setEssMaxCharge(ctrlContext.maxSocEnergyInChargeGrid - simContext.getEssInitial()); + applyChargeGrid(energyFlow, ctrlContext.essChargeInChargeGrid); + } + } + }, // + Utils::postprocessSimulatorState); + } + + /** + * Simulate {@link EnergyFlow} in {@link StateMachine#BALANCING}. + * + * @param model the {@link EnergyFlow.Model} + */ + public static void applyBalancing(EnergyFlow.Model model) { + var target = model.consumption - model.production; + model.setFittingCoefficientValue(ESS, EQ, target); + } + + /** + * Simulate {@link EnergyFlow} in DELAY_DISCHARGE. + * + * @param model the {@link EnergyFlow.Model} + */ + public static void applyDelayDischarge(EnergyFlow.Model model) { + var target = min(0 /* Charge -> apply Balancing */, model.consumption - model.production); + model.setFittingCoefficientValue(ESS, EQ, target); + } + + /** + * Simulate {@link EnergyFlow} in {@link StateMachine#CHARGE_GRID}. + * + * @param model the {@link EnergyFlow.Model} + * @param chargeEnergy the target charge-from-grid energy + */ + public static void applyChargeGrid(EnergyFlow.Model model, int chargeEnergy) { + model.setExtremeCoefficientValue(PROD_TO_ESS, MAXIMIZE); + model.setExtremeCoefficientValue(GRID_TO_CONS, MAXIMIZE); + model.setFittingCoefficientValue(GRID_TO_ESS, EQ, chargeEnergy); + } + + /** + * Simulate {@link EnergyFlow} in a future DISCHARGE_GRID state. + * + * @param model the {@link EnergyFlow.Model} + * @param dischargeEnergy the target discharge-to-grid energy + */ + public static void applyDischargeGrid(EnergyFlow.Model model, int dischargeEnergy) { + model.setExtremeCoefficientValue(PROD_TO_GRID, MAXIMIZE); + model.setFittingCoefficientValue(GRID_TO_ESS, EQ, -dischargeEnergy); + } + + public static record EshContext(ManagedSymmetricEss ess, ControlMode controlMode, int maxChargePowerFromGrid, + boolean limitChargePowerFor14aEnWG, int maxSocEnergyInChargeGrid, int essChargeInChargeGrid) { + } + @Override - public EnergyScheduleHandler getEnergyScheduleHandler() { + public EnergyScheduleHandler.WithDifferentStates getEnergyScheduleHandler() { return this.energyScheduleHandler; } + + @Override + public EnergyScheduleHandlerV1 getEnergyScheduleHandlerV1() { + return this.energyScheduleHandlerV1; + } } diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Utils.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Utils.java index 6543f9b24c1..7b21d653571 100644 --- a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Utils.java +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Utils.java @@ -1,23 +1,27 @@ package io.openems.edge.controller.ess.timeofusetariff; -import static io.openems.edge.common.type.TypeUtils.multiply; +import static com.google.common.math.Quantiles.percentiles; import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.CHARGE_GRID; import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; -import static io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffController.PERIODS_PER_HOUR; +import static io.openems.edge.energy.api.EnergyConstants.PERIODS_PER_HOUR; +import static io.openems.edge.energy.api.EnergyUtils.findFirstPeakIndex; +import static io.openems.edge.energy.api.EnergyUtils.findFirstValleyIndex; +import static io.openems.edge.energy.api.EnergyUtils.toPower; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.Math.round; -import static java.util.stream.IntStream.concat; +import static java.util.Arrays.stream; -import java.util.List; -import java.util.Objects; +import com.google.common.primitives.ImmutableIntArray; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.Sum; import io.openems.edge.controller.api.Controller; -import io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve; -import io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischarge; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.EshContext; +import io.openems.edge.energy.api.EnergyScheduleHandler.WithDifferentStates.Period; +import io.openems.edge.energy.api.simulation.EnergyFlow; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; import io.openems.edge.ess.api.HybridEss; import io.openems.edge.ess.api.ManagedSymmetricEss; @@ -52,29 +56,6 @@ private Utils() { public static final ChannelAddress SUM_ESS_DISCHARGE_POWER = new ChannelAddress("_sum", "EssDischargePower"); public static final ChannelAddress SUM_ESS_SOC = new ChannelAddress("_sum", "EssSoc"); - /** - * Returns the configured minimum SoC, or zero. - * - * @param ctrlLimitTotalDischarges the list of - * {@link ControllerEssLimitTotalDischarge} - * @param ctrlEmergencyCapacityReserves the list of - * {@link ControllerEssEmergencyCapacityReserve} - * @return the value in [%] - */ - public static int getEssMinSocPercentage(List ctrlLimitTotalDischarges, - List ctrlEmergencyCapacityReserves) { - return concat(// - ctrlLimitTotalDischarges.stream() // - .map(ctrl -> ctrl.getMinSoc().get()) // - .filter(Objects::nonNull) // - .mapToInt(v -> max(0, v)), // only positives - ctrlEmergencyCapacityReserves.stream() // - .map(ctrl -> ctrl.getActualReserveSoc().get()) // - .filter(Objects::nonNull) // - .mapToInt(v -> max(0, v))) // only positives - .max().orElse(0); - } - public static record ApplyState(StateMachine actualState, Integer setPoint) { } @@ -83,20 +64,19 @@ public static record ApplyState(StateMachine actualState, Integer setPoint) { * * @param sum the {@link Sum} * @param ess the {@link ManagedSymmetricEss} - * @param essChargeInChargeGrid ESS Charge Energy in CHARGE_GRID State [Wh] * @param maxChargePowerFromGrid the configured max charge from grid power * @param limitChargePowerFor14aEnWG Limit Charge Power for §14a EnWG - * @param targetState the scheduled target {@link StateMachine} + * @param period the scheduled {@link Period} * @return {@link ApplyState} */ - public static ApplyState calculateAutomaticMode(Sum sum, ManagedSymmetricEss ess, Integer essChargeInChargeGrid, - int maxChargePowerFromGrid, boolean limitChargePowerFor14aEnWG, StateMachine targetState) { + public static ApplyState calculateAutomaticMode(Sum sum, ManagedSymmetricEss ess, int maxChargePowerFromGrid, + boolean limitChargePowerFor14aEnWG, Period period) { final StateMachine actualState; final Integer setPoint; var gridActivePower = sum.getGridActivePower().get(); // current buy-from/sell-to grid var essActivePower = ess.getActivePower().get(); // current charge/discharge ESS - if (gridActivePower == null || essActivePower == null) { + if (period == null || gridActivePower == null || essActivePower == null) { // undefined state return new ApplyState(BALANCING, null); } @@ -104,9 +84,9 @@ public static ApplyState calculateAutomaticMode(Sum sum, ManagedSymmetricEss ess // Post-process and get actual state final var pwrBalancing = gridActivePower + essActivePower; final var pwrDelayDischarge = calculateDelayDischargePower(ess); - final var pwrChargeGrid = calculateChargeGridPower(essChargeInChargeGrid, ess, essActivePower, gridActivePower, - maxChargePowerFromGrid, limitChargePowerFor14aEnWG); - actualState = postprocessRunState(targetState, pwrBalancing, pwrDelayDischarge, pwrChargeGrid); + final var pwrChargeGrid = calculateChargeGridPower(period.context().essChargeInChargeGrid(), ess, + essActivePower, gridActivePower, maxChargePowerFromGrid, limitChargePowerFor14aEnWG); + actualState = postprocessRunState(period.state(), pwrBalancing, pwrDelayDischarge, pwrChargeGrid); // Get and apply ActivePower Less-or-Equals Set-Point setPoint = switch (actualState) { @@ -156,8 +136,39 @@ public static StateMachine postprocessRunState(StateMachine state, int pwrBalanc return state; } - protected static int calculateEssChargeInChargeGridPowerFromParams(Integer essChargeInChargeGrid, - ManagedSymmetricEss ess) { + /** + * Post-Process a state of a Period during Simulation, i.e. replace with + * 'better' state with the same behaviour. + * + *

+ * NOTE: heavy computation is ok here, because this method is called only at the + * end with the best Schedule. + * + * @param ef the {@link EnergyFlow} for the state + * @param state the initial state + * @return the new state + */ + public static StateMachine postprocessSimulatorState(EnergyFlow ef, StateMachine state) { + if (state == CHARGE_GRID) { + // CHARGE_GRID,... + if (ef.getGridToEss() <= 0) { + // but battery is not charged from grid + state = DELAY_DISCHARGE; + } + } + + if (state == DELAY_DISCHARGE) { + // DELAY_DISCHARGE,... + if (ef.getEss() < 0) { + // but battery gets charged + state = BALANCING; + } + } + + return state; + } + + protected static int calculateEssChargeInChargeGridPower(Integer essChargeInChargeGrid, ManagedSymmetricEss ess) { if (essChargeInChargeGrid != null) { return toPower(essChargeInChargeGrid); } @@ -187,7 +198,7 @@ protected static int calculateEssChargeInChargeGridPowerFromParams(Integer essCh public static int calculateChargeGridPower(Integer essChargeInChargeGrid, ManagedSymmetricEss ess, int essActivePower, int gridActivePower, int maxChargePowerFromGrid, boolean limitChargePowerFor14aEnWG) { var realGridPower = gridActivePower + essActivePower; // 'real', without current ESS charge/discharge - var targetChargePower = calculateEssChargeInChargeGridPowerFromParams(essChargeInChargeGrid, ess) // + var targetChargePower = calculateEssChargeInChargeGridPower(essChargeInChargeGrid, ess) // + min(0, realGridPower) * -1; // add excess production var effectiveGridBuyPower = max(0, realGridPower) + targetChargePower; var chargePower = max(0, targetChargePower - max(0, effectiveGridBuyPower - maxChargePowerFromGrid)); @@ -236,12 +247,63 @@ public static int calculateDelayDischargePower(ManagedSymmetricEss ess) { } /** - * Converts energy [Wh/15 min] to power [W]. + * Calculates the default ESS charge energy per period in + * {@link StateMachine#CHARGE_GRID}. * - * @param energy the energy value - * @return the power value + *

+ * Applies {@link #ESS_CHARGE_C_RATE} with the minimum of usable ESS energy or + * predicted consumption energy that cannot be supplied from production. + * + * @param gsc the {@link GlobalSimulationsContext} + * @return the value in [Wh] */ - private static Integer toPower(Integer energy) { - return multiply(energy, PERIODS_PER_HOUR); + public static int calculateChargeEnergyInChargeGrid(GlobalSimulationsContext gsc) { + var refs = ImmutableIntArray.builder(); + + // Uses the total available energy as reference (= fallback) + var fallback = max(0, round(ESS_MAX_SOC / 100F * gsc.ess().totalEnergy())); + add(refs, fallback); + + // Uses the total excess consumption as reference + add(refs, gsc.periods().stream() // + .mapToInt(p -> p.consumption() - p.production()) // calculates excess Consumption Energy per Period + .sum()); + + add(refs, gsc.periods().stream() // + .takeWhile(p -> p.consumption() >= p.production()) // take only first Periods + .mapToInt(p -> p.consumption() - p.production()) // calculates excess Consumption Energy per Period + .sum()); + + // Uses the excess consumption during high price periods as reference + { + var prices = gsc.periods().stream() // + .mapToDouble(GlobalSimulationsContext.Period::price) // + .toArray(); + var peakIndex = findFirstPeakIndex(findFirstValleyIndex(0, prices), prices); + var firstPrices = stream(prices) // + .limit(peakIndex) // + .toArray(); + if (firstPrices.length > 0) { + var percentilePrice = percentiles().index(95).compute(firstPrices); + add(refs, gsc.periods().stream() // + .limit(peakIndex) // + .filter(p -> p.price() >= percentilePrice) // takes only prices > percentile + .mapToInt(p -> p.consumption() - p.production()) // excess Consumption Energy per Period + .sum()); + } + } + + return (int) round(// + refs.build().stream() // + .average() // + .orElse(fallback) // + * ESS_CHARGE_C_RATE / PERIODS_PER_HOUR); } + + private static void add(ImmutableIntArray.Builder builder, int value) { + if (value > 0) { + builder.add(value); + } + } + } diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/jsonrpc/GetScheduleRequest.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleRequest.java similarity index 94% rename from io.openems.edge.energy/src/io/openems/edge/energy/jsonrpc/GetScheduleRequest.java rename to io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleRequest.java index 1dbdb0dc7d8..ff357bfd827 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/jsonrpc/GetScheduleRequest.java +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleRequest.java @@ -1,4 +1,4 @@ -package io.openems.edge.energy.jsonrpc; +package io.openems.edge.controller.ess.timeofusetariff.jsonrpc; import com.google.gson.JsonObject; diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponse.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponse.java new file mode 100644 index 00000000000..d60e3eae6e6 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponse.java @@ -0,0 +1,230 @@ +package io.openems.edge.controller.ess.timeofusetariff.jsonrpc; + +import static io.openems.common.utils.JsonUtils.buildJsonObject; +import static io.openems.common.utils.JsonUtils.getAsOptionalDouble; +import static io.openems.common.utils.JsonUtils.getAsOptionalInt; +import static io.openems.common.utils.JsonUtils.toJsonArray; +import static io.openems.edge.common.type.TypeUtils.fitWithin; +import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.SUM_CONSUMPTION; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.SUM_ESS_DISCHARGE_POWER; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.SUM_ESS_SOC; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.SUM_GRID; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.SUM_PRODUCTION; +import static io.openems.edge.energy.api.EnergyUtils.toPower; +import static java.lang.Math.round; +import static java.util.Optional.ofNullable; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Set; +import java.util.SortedMap; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableSortedMap; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; +import io.openems.common.timedata.Resolution; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.controller.ess.timeofusetariff.StateMachine; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffController; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.EshContext; +import io.openems.edge.controller.ess.timeofusetariff.Utils; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.EnergyScheduleHandler.WithDifferentStates.Period; +import io.openems.edge.ess.api.SymmetricEss; +import io.openems.edge.timedata.api.Timedata; + +/** + * Represents a JSON-RPC Response for 'getSchedule'. + * + *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": "UUID",
+ *   "result": {
+ *     'schedule': [{
+ *      'timestamp':...,
+ *      'price':...,
+ *      'state':...,
+ *      'grid':...,
+ *      'production':...,
+ *      'consumption':...,
+ *      'ess':...,
+ *      'soc':...,
+ *     }]
+ *   }
+ * }
+ * 
+ */ +public class GetScheduleResponse extends JsonrpcResponseSuccess { + + private static final Logger LOG = LoggerFactory.getLogger(GetScheduleResponse.class); + + private final JsonObject result; + + public GetScheduleResponse(UUID id, JsonObject result) { + super(id); + this.result = result; + } + + @Override + public JsonObject getResult() { + return this.result; + } + + /** + * Builds a {@link GetScheduleResponse} with last three hours data and current + * Schedule. + * + * @param requestId the JSON-RPC request-id + * @param componentId the Component-ID of the parent + * {@link TimeOfUseTariffController} + * @param clock a {@link Clock} + * @param ess the {@link SymmetricEss} + * @param timedata the {@link Timedata} + * @param energyScheduleHandler the {@link EnergyScheduleHandler} + * @return the {@link GetScheduleResponse} + * @throws OpenemsNamedException on error + */ + public static GetScheduleResponse from(UUID requestId, String componentId, Clock clock, SymmetricEss ess, + Timedata timedata, + EnergyScheduleHandler.WithDifferentStates energyScheduleHandler) { + final var schedule = energyScheduleHandler.getSchedule(); + final JsonArray result; + if (schedule.isEmpty()) { + result = new JsonArray(); + } else { + final var historic = fromHistoricData(componentId, schedule.firstKey(), timedata); + final var future = fromSchedule(ess, schedule); + result = Stream.concat(historic, future) // + .collect(toJsonArray()); + } + + return new GetScheduleResponse(requestId, // + buildJsonObject() // + .add("schedule", result) // + .build()); + } + + /** + * Queries the last three hours' data and converts it to a {@link Stream} of + * {@link JsonObject}s suitable for a {@link GetScheduleResponse}. + * + * @param componentId Component-ID of {@link TimeOfUseTariffControllerImpl} + * @param firstSchedule {@link ZonedDateTime} of the first entry in the Schedule + * (rounded down to 15 minutes) + * @param timedata the {@link Timedata} + * @return {@link Stream} of {@link JsonObject}s + */ + // TODO protected is sufficient after v1 + public static Stream fromHistoricData(String componentId, ZonedDateTime firstSchedule, + Timedata timedata) { + // Process last three hours of historic data + final var fromTime = firstSchedule.minusHours(3); + final var toTime = firstSchedule.minusSeconds(1); + final var channelQuarterlyPrices = new ChannelAddress(componentId, "QuarterlyPrices"); + final var channelStateMachine = new ChannelAddress(componentId, "StateMachine"); + SortedMap> data = null; + try { + data = timedata.queryHistoricData(null, fromTime, toTime, // + Set.of(channelQuarterlyPrices, channelStateMachine, // + Utils.SUM_GRID, SUM_PRODUCTION, SUM_CONSUMPTION, SUM_ESS_DISCHARGE_POWER, SUM_ESS_SOC), + new Resolution(15, ChronoUnit.MINUTES)); + } catch (Exception e) { + LOG.warn("Unable to read historic data: " + e.getMessage()); + } + if (data == null) { + return Stream.of(); + } + + return data.entrySet().stream() // + .map(e -> { + var d = e.getValue(); + Function getter = (c) -> ofNullable(d.get(c)) + .orElse(JsonNull.INSTANCE); + + return buildJsonObject() // + .addProperty("timestamp", e.getKey()) // + .addProperty("price", + getAsOptionalDouble(getter.apply(channelQuarterlyPrices)).orElse(null)) // + .addProperty("state", + getAsOptionalInt(getter.apply(channelStateMachine)).orElse(BALANCING.getValue())) // + .addProperty("grid", getAsOptionalInt(getter.apply(SUM_GRID)).orElse(null)) // + .addProperty("production", getAsOptionalInt(getter.apply(SUM_PRODUCTION)).orElse(null)) // + .addProperty("consumption", getAsOptionalInt(getter.apply(SUM_CONSUMPTION)).orElse(null)) // + .addProperty("ess", getAsOptionalInt(getter.apply(SUM_ESS_DISCHARGE_POWER)).orElse(null)) // + .addProperty("soc", getAsOptionalInt(getter.apply(SUM_ESS_SOC)).orElse(null)) // + .build(); + }); + } + + /** + * Converts the Schedule to a {@link Stream} of {@link JsonObject}s suitable for + * a {@link GetScheduleResponse}. + * + * @param ess the {@link SymmetricEss} + * @param schedule the {@link EnergyScheduleHandler} schedule + * @return {@link Stream} of {@link JsonObject}s + */ + protected static Stream fromSchedule(SymmetricEss ess, + ImmutableSortedMap> schedule) { + final var essTotalEnergy = ess.getCapacity().orElse(0); + return schedule.entrySet().stream() // + .map(e -> { + var p = e.getValue(); + + return buildJsonObject() // + .addProperty("timestamp", e.getKey()) // + .addProperty("price", p.price()) // + .addProperty("state", p.state().getValue()) // + .addProperty("grid", toPower(p.energyFlow().getGrid())) // + .addProperty("production", toPower(p.energyFlow().getProd())) // + .addProperty("consumption", toPower(p.energyFlow().getCons())) // + .addProperty("ess", toPower(p.energyFlow().getEss())) // + .addProperty("soc", round(fitWithin(0F, 100F, // + p.essInitialEnergy() * 100F / essTotalEnergy))) // + .build(); + }); + } + + /** + * Creates an empty default Schedule in case no Schedule is available. + * + * @param clock the {@link Clock} + * @param defaultState the default {@link StateMachine} + * @return {@link Stream} of {@link JsonObject}s + */ + protected static Stream empty(Clock clock, StateMachine defaultState) { + final var now = ZonedDateTime.now(clock); + final var numberOfPeriods = 96; + + return IntStream.range(0, numberOfPeriods) // + .mapToObj(i -> { + return buildJsonObject() // + .addProperty("timestamp", now.plusMinutes(i * 15)) // + .add("price", JsonNull.INSTANCE) // + .addProperty("state", defaultState.getValue()) // + .add("grid", JsonNull.INSTANCE) // + .add("production", JsonNull.INSTANCE) // + .add("consumption", JsonNull.INSTANCE) // + .add("ess", JsonNull.INSTANCE) // + .add("soc", JsonNull.INSTANCE) // + .build(); + }); + } + +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/package-info.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/package-info.java new file mode 100644 index 00000000000..c3c8d61c926 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.controller.ess.timeofusetariff.jsonrpc; diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/v1/EnergyScheduleHandlerV1.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/v1/EnergyScheduleHandlerV1.java new file mode 100644 index 00000000000..305fa7ee007 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/v1/EnergyScheduleHandlerV1.java @@ -0,0 +1,88 @@ +package io.openems.edge.controller.ess.timeofusetariff.v1; + +import static io.openems.common.utils.DateUtils.roundDownToQuarter; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableSortedMap; + +import io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve; +import io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischarge; +import io.openems.edge.controller.ess.timeofusetariff.ControlMode; +import io.openems.edge.controller.ess.timeofusetariff.StateMachine; +import io.openems.edge.ess.api.ManagedSymmetricEss; + +@Deprecated +public class EnergyScheduleHandlerV1 { + + public static record ContextV1(List ctrlEmergencyCapacityReserves, + List ctrlLimitTotalDischarges, ManagedSymmetricEss ess, + ControlMode controlMode, int maxChargePowerFromGrid, boolean limitChargePowerFor14aEnWG) { + } + + private final Supplier availableStates; + private final Supplier context; + + private ImmutableSortedMap> schedule = ImmutableSortedMap.of(); + + public EnergyScheduleHandlerV1(Supplier availableStates, Supplier context) { + this.availableStates = availableStates; + this.context = context; + } + + /** + * Gets the available States. + * + * @return an Array of States + */ + public StateMachine[] getAvailableStates() { + return this.availableStates.get(); + } + + /** + * Gets the Context. + * + * @return the Context + */ + public ContextV1 getContext() { + return this.context.get(); + } + + public static record Period(STATE state, Integer essChargeInChargeGrid) { + } + + /** + * Sets the Schedule. Called by Optimizer. + * + * @param schedule the Schedule + */ + public synchronized void setSchedule(ImmutableSortedMap> schedule) { + this.schedule = schedule; + } + + /** + * Gets the current State or null. + * + * @return the State or null + */ + public synchronized StateMachine getCurrentState() { + return Optional.ofNullable(this.schedule.get(roundDownToQuarter(ZonedDateTime.now()))) // + .map(Period::state) // + .orElse(null); + } + + /** + * Gets the current essChargeInChargeGrid or null. + * + * @return the essChargeInChargeGrid or null + */ + public synchronized Integer getCurrentEssChargeInChargeGrid() { + return Optional.ofNullable(this.schedule.get(roundDownToQuarter(ZonedDateTime.now()))) // + .map(Period::essChargeInChargeGrid) // + .orElse(null); + } + +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/v1/UtilsV1.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/v1/UtilsV1.java new file mode 100644 index 00000000000..56db25223e9 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/v1/UtilsV1.java @@ -0,0 +1,132 @@ +package io.openems.edge.controller.ess.timeofusetariff.v1; + +import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateChargeGridPower; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateDelayDischargePower; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.postprocessRunState; +import static java.lang.Math.max; +import static java.util.stream.IntStream.concat; + +import java.util.List; +import java.util.Objects; + +import io.openems.edge.common.sum.Sum; +import io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve; +import io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischarge; +import io.openems.edge.controller.ess.timeofusetariff.StateMachine; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffController; +import io.openems.edge.controller.ess.timeofusetariff.Utils.ApplyState; +import io.openems.edge.ess.api.ManagedSymmetricEss; + +/** + * Utils for {@link TimeOfUseTariffController}. + * + *

+ * All energy values are in [Wh] and positive, unless stated differently. + */ +@Deprecated +public final class UtilsV1 { + + private UtilsV1() { + } + + public static final int PERIODS_PER_HOUR = 4; + + /** + * Returns the configured minimum SoC, or zero. + * + * @param ctrlLimitTotalDischarges the list of + * {@link ControllerEssLimitTotalDischarge} + * @param ctrlEmergencyCapacityReserves the list of + * {@link ControllerEssEmergencyCapacityReserve} + * @return the value in [%] + */ + public static int getEssMinSocPercentage(List ctrlLimitTotalDischarges, + List ctrlEmergencyCapacityReserves) { + return concat(// + ctrlLimitTotalDischarges.stream() // + .map(ctrl -> ctrl.getMinSoc().get()) // + .filter(Objects::nonNull) // + .mapToInt(v -> max(0, v)), // only positives + ctrlEmergencyCapacityReserves.stream() // + .map(ctrl -> ctrl.getActualReserveSoc().get()) // + .filter(Objects::nonNull) // + .mapToInt(v -> max(0, v))) // only positives + .max().orElse(0); + } + + /** + * Calculate Automatic Mode. + * + * @param esh the {@link EnergyScheduleHandlerV1} + * @param sum the {@link Sum} + * @param ess the {@link ManagedSymmetricEss} + * @param maxChargePowerFromGrid the configured max charge from grid power + * @param limitChargePowerFor14aEnWG Limit Charge Power for §14a EnWG + * @return {@link ApplyState} + */ + public static ApplyState calculateAutomaticMode(EnergyScheduleHandlerV1 esh, Sum sum, ManagedSymmetricEss ess, + int maxChargePowerFromGrid, boolean limitChargePowerFor14aEnWG) { + final var targetState = getCurrentPeriodState(esh); + final var essChargeInChargeGrid = esh.getCurrentEssChargeInChargeGrid(); + return calculateAutomaticMode(sum, ess, essChargeInChargeGrid, maxChargePowerFromGrid, + limitChargePowerFor14aEnWG, targetState); + } + + /** + * Calculate Automatic Mode. + * + * @param sum the {@link Sum} + * @param ess the {@link ManagedSymmetricEss} + * @param essChargeInChargeGrid ESS Charge Energy in CHARGE_GRID State [Wh] + * @param maxChargePowerFromGrid the configured max charge from grid power + * @param limitChargePowerFor14aEnWG Limit Charge Power for §14a EnWG + * @param targetState the scheduled target {@link StateMachine} + * @return {@link ApplyState} + */ + protected static ApplyState calculateAutomaticMode(Sum sum, ManagedSymmetricEss ess, Integer essChargeInChargeGrid, + int maxChargePowerFromGrid, boolean limitChargePowerFor14aEnWG, StateMachine targetState) { + final StateMachine actualState; + final Integer setPoint; + + var gridActivePower = sum.getGridActivePower().get(); // current buy-from/sell-to grid + var essActivePower = ess.getActivePower().get(); // current charge/discharge ESS + if (gridActivePower == null || essActivePower == null) { + // undefined state + return new ApplyState(BALANCING, null); + } + + // Post-process and get actual state + final var pwrBalancing = gridActivePower + essActivePower; + final var pwrDelayDischarge = calculateDelayDischargePower(ess); + final var pwrChargeGrid = calculateChargeGridPower(essChargeInChargeGrid, ess, essActivePower, gridActivePower, + maxChargePowerFromGrid, limitChargePowerFor14aEnWG); + actualState = postprocessRunState(targetState, pwrBalancing, pwrDelayDischarge, pwrChargeGrid); + + // Get and apply ActivePower Less-or-Equals Set-Point + setPoint = switch (actualState) { + case BALANCING -> null; // delegate to next priority Controller + case DELAY_DISCHARGE -> pwrDelayDischarge; + case CHARGE_GRID -> pwrChargeGrid; + }; + + return new ApplyState(actualState, setPoint); + } + + /** + * Gets the current period state of the {@link EnergyScheduleHandlerV1} or + * {@link StateMachine#BALANCING}. + * + * @param esh the {@link EnergyScheduleHandlerV1} + * @return the {@link StateMachine} + */ + public static StateMachine getCurrentPeriodState(EnergyScheduleHandlerV1 esh) { + if (esh != null) { + var state = esh.getCurrentState(); + if (state != null) { + return state; + } + } + return BALANCING; // Default Fallback + } +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/v1/package-info.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/v1/package-info.java new file mode 100644 index 00000000000..7c842e31832 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/v1/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.controller.ess.timeofusetariff.v1; diff --git a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/MyConfig.java b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/MyConfig.java index bc0c137b4a2..60f8bef585d 100644 --- a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/MyConfig.java +++ b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/MyConfig.java @@ -2,6 +2,7 @@ import io.openems.common.test.AbstractComponentConfig; import io.openems.common.utils.ConfigUtils; +import io.openems.edge.energy.api.Version; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { @@ -16,6 +17,7 @@ protected static class Builder { private int maxChargePowerFromGrid; private boolean limitChargePowerFor14aEnWG; private RiskLevel riskLevel; + private Version version; private Builder() { } @@ -65,6 +67,11 @@ public Builder setLimitChargePowerFor14aEnWG(boolean limitChargePowerFor14aEnWG) return this; } + public Builder setVersion(Version version) { + this.version = version; + return this; + } + public MyConfig build() { return new MyConfig(this); } @@ -121,6 +128,11 @@ public RiskLevel riskLevel() { return this.builder.riskLevel; } + @Override + public Version version() { + return this.builder.version; + } + @Override public String ess_target() { return ConfigUtils.generateReferenceTargetFilter(this.id(), this.ess_id()); diff --git a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImplTest.java b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImplTest.java index d3f32289678..3750aa110a7 100644 --- a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImplTest.java +++ b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImplTest.java @@ -1,44 +1,52 @@ package io.openems.edge.controller.ess.timeofusetariff; +import static io.openems.edge.common.test.TestUtils.createDummyClock; import static io.openems.edge.controller.ess.timeofusetariff.ControlMode.CHARGE_CONSUMPTION; import static io.openems.edge.controller.ess.timeofusetariff.Mode.AUTOMATIC; import static io.openems.edge.controller.ess.timeofusetariff.RiskLevel.MEDIUM; import java.time.Clock; -import java.time.Instant; -import java.time.ZoneOffset; import org.junit.Test; -import io.openems.common.test.TimeLeapClock; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.EshContext; import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.Version; +import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.test.DummyTimedata; import io.openems.edge.timeofusetariff.test.DummyTimeOfUseTariffProvider; public class TimeOfUseTariffControllerImplTest { - public static final Clock CLOCK = new TimeLeapClock(Instant.parse("2020-03-04T14:19:00.00Z"), ZoneOffset.UTC); - - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { - create(CLOCK); + final var clock = createDummyClock(); + create(clock, // + new DummyManagedSymmetricEss("ess0") // + .withSoc(60) // + .withCapacity(10000), // + new DummyTimedata("timedata0")) // + .deactivate(); } /** * Creates a {@link TimeOfUseTariffControllerImpl} instance. * - * @param clock a {@link Clock} + * @param clock a {@link Clock} + * @param ess the {@link SymmetricEss} + * @param timedata the {@link Timedata} * @return the object * @throws Exception on error */ - public static TimeOfUseTariffControllerImpl create(Clock clock) throws Exception { + public static TimeOfUseTariffControllerImpl create(Clock clock, SymmetricEss ess, Timedata timedata) + throws Exception { var componentManager = new DummyComponentManager(clock); var sum = new DummySum(); var timeOfUseTariff = DummyTimeOfUseTariffProvider.empty(clock); @@ -47,14 +55,12 @@ public static TimeOfUseTariffControllerImpl create(Clock clock) throws Exception new ControllerTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", componentManager) // - .addReference("timedata", new DummyTimedata("timedata0")) // + .addReference("timedata", timedata) // .addReference("timeOfUseTariff", timeOfUseTariff) // .addReference("sum", sum) // - .addReference("ess", new DummyManagedSymmetricEss("ess0") // - .withSoc(60) // - .withCapacity(10000)) // + .addReference("ess", ess) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setEnabled(false) // .setEssId("ess0") // .setMode(AUTOMATIC) // @@ -62,9 +68,22 @@ public static TimeOfUseTariffControllerImpl create(Clock clock) throws Exception .setEssMaxChargePower(5000) // .setMaxChargePowerFromGrid(10000) // .setLimitChargePowerFor14aEnWG(false) // + .setVersion(Version.V2_ENERGY_SCHEDULABLE) // .setRiskLevel(MEDIUM) // .build()) // .next(new TestCase()); return sut; } + + /** + * Gets the {@link EnergyScheduleHandler}. + * + * @param ctrl the {@link TimeOfUseTariffControllerImpl} + * @return the object + * @throws Exception on error + */ + public static EnergyScheduleHandler.WithDifferentStates getEnergyScheduleHandler( + TimeOfUseTariffControllerImpl ctrl) throws Exception { + return ctrl.getEnergyScheduleHandler(); + } } diff --git a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/UtilsTest.java b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/UtilsTest.java index c7a104c3419..ef883691f38 100644 --- a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/UtilsTest.java +++ b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/UtilsTest.java @@ -5,22 +5,36 @@ import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.CHARGE_GRID; import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateAutomaticMode; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateChargeEnergyInChargeGrid; import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateChargeGridPower; import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateDelayDischargePower; -import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateEssChargeInChargeGridPowerFromParams; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateEssChargeInChargeGridPower; import static io.openems.edge.controller.ess.timeofusetariff.Utils.calculateMaxChargeProductionPower; import static org.junit.Assert.assertEquals; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + import org.junit.Test; +import com.google.common.collect.ImmutableList; + +import io.openems.common.test.TimeLeapClock; import io.openems.edge.common.sum.DummySum; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.EshContext; import io.openems.edge.controller.ess.timeofusetariff.Utils.ApplyState; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyHybridEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; public class UtilsTest { + public static final TimeLeapClock CLOCK = new TimeLeapClock(Instant.ofEpochSecond(946684800), ZoneId.of("UTC")); + public static final ZonedDateTime TIME = ZonedDateTime.now(CLOCK); + @Test public void testCalculateChargeGridPower() { assertEquals(-10000, calculateChargeGridPower(null, // @@ -111,22 +125,28 @@ public void testCalculateDelayDischarge() { } @Test - public void testCalculateMaxChargeGridPowerFromParams() { + public void testCalculateMaxChargeGridPower() { final var ess = new DummyManagedSymmetricEss("ess0"); // No params, initial ESS - assertEquals(0, calculateEssChargeInChargeGridPowerFromParams(null, ess)); + assertEquals(0, calculateEssChargeInChargeGridPower(null, ess)); // No params, ESS with MaxApparentPower withValue(ess, SymmetricEss.ChannelId.MAX_APPARENT_POWER, 1000); - assertEquals(250, calculateEssChargeInChargeGridPowerFromParams(null, ess)); + assertEquals(250, calculateEssChargeInChargeGridPower(null, ess)); // No params, ESS with Capacity withValue(ess, SymmetricEss.ChannelId.CAPACITY, 15000); - assertEquals(7500, calculateEssChargeInChargeGridPowerFromParams(null, ess)); + assertEquals(7500, calculateEssChargeInChargeGridPower(null, ess)); // With params (22 kWh; but few Consumption) - assertEquals(5360, calculateEssChargeInChargeGridPowerFromParams(1340, ess)); + assertEquals(5360, calculateEssChargeInChargeGridPower(1340, ess)); + } + + private static EnergyScheduleHandler.WithDifferentStates.Period mockPeriod( + StateMachine state, int essChargeInChargeGrid) { + return new EnergyScheduleHandler.WithDifferentStates.Period(state, 0, + new EshContext(null, null, 0, false, 0, essChargeInChargeGrid), null, 0); } @Test @@ -135,19 +155,17 @@ public void testCalculateAutomaticMode() { calculateAutomaticMode(// new DummySum(), // new DummyManagedSymmetricEss("ess0"), // - /* essChargeInChargeGrid */ 1000, // /* maxChargePowerFromGrid */ 2000, // /* limitChargePowerFor14aEnWG */ true, // - BALANCING)); + mockPeriod(BALANCING, /* essChargeInChargeGrid */ 1000))); assertEquals("Null-Check", new ApplyState(BALANCING, null), // calculateAutomaticMode(// new DummySum() // .withGridActivePower(100), // new DummyManagedSymmetricEss("ess0"), // - /* essChargeInChargeGrid */ 1000, // /* maxChargePowerFromGrid */ 2000, // /* limitChargePowerFor14aEnWG */ true, // - BALANCING)); + mockPeriod(BALANCING, /* essChargeInChargeGrid */ 1000))); assertEquals("BALANCING", new ApplyState(BALANCING, null), // calculateAutomaticMode(// @@ -155,10 +173,9 @@ public void testCalculateAutomaticMode() { .withGridActivePower(100), // new DummyManagedSymmetricEss("ess0") // .withActivePower(500), // - /* essChargeInChargeGrid */ 1000, // /* maxChargePowerFromGrid */ 2000, // /* limitChargePowerFor14aEnWG */ true, // - BALANCING)); + mockPeriod(BALANCING, /* essChargeInChargeGrid */ 1000))); assertEquals("DELAY_DISCHARGE stays DELAY_DISCHARGE", new ApplyState(DELAY_DISCHARGE, 0), // calculateAutomaticMode(// @@ -166,20 +183,19 @@ public void testCalculateAutomaticMode() { .withGridActivePower(100), // new DummyManagedSymmetricEss("ess0") // .withActivePower(500), // - /* essChargeInChargeGrid */ 1000, // /* maxChargePowerFromGrid */ 2000, // /* limitChargePowerFor14aEnWG */ true, // - DELAY_DISCHARGE)); + mockPeriod(DELAY_DISCHARGE, /* essChargeInChargeGrid */ 1000))); + assertEquals("DELAY_DISCHARGE to BALANCING", new ApplyState(BALANCING, null), // calculateAutomaticMode(// new DummySum() // .withGridActivePower(-500), // new DummyManagedSymmetricEss("ess0") // .withActivePower(500), // - /* essChargeInChargeGrid */ 1000, // /* maxChargePowerFromGrid */ 2000, // /* limitChargePowerFor14aEnWG */ true, // - DELAY_DISCHARGE)); + mockPeriod(DELAY_DISCHARGE, /* essChargeInChargeGrid */ 1000))); assertEquals("CHARGE_GRID stays CHARGE_GRID", new ApplyState(CHARGE_GRID, -1400), // calculateAutomaticMode(// @@ -187,29 +203,78 @@ public void testCalculateAutomaticMode() { .withGridActivePower(100), // new DummyManagedSymmetricEss("ess0") // .withActivePower(500), // - /* essChargeInChargeGrid */ 1000, // /* maxChargePowerFromGrid */ 2000, // /* limitChargePowerFor14aEnWG */ true, // - CHARGE_GRID)); + mockPeriod(CHARGE_GRID, /* essChargeInChargeGrid */ 1000))); + assertEquals("CHARGE_GRID to DELAY_DISCHARGE", new ApplyState(DELAY_DISCHARGE, 0), // calculateAutomaticMode(// new DummySum() // .withGridActivePower(100), // new DummyManagedSymmetricEss("ess0") // .withActivePower(500), // - /* essChargeInChargeGrid */ 1000, // /* maxChargePowerFromGrid */ 400, // /* limitChargePowerFor14aEnWG */ true, // - CHARGE_GRID)); + mockPeriod(CHARGE_GRID, /* essChargeInChargeGrid */ 1000))); + assertEquals("CHARGE_GRID to BALANCING", new ApplyState(BALANCING, null), // calculateAutomaticMode(// new DummySum() // .withGridActivePower(-500), // new DummyManagedSymmetricEss("ess0") // .withActivePower(500), // - /* essChargeInChargeGrid */ 1000, // /* maxChargePowerFromGrid */ 0, // /* limitChargePowerFor14aEnWG */ true, // - CHARGE_GRID)); + mockPeriod(CHARGE_GRID, /* essChargeInChargeGrid */ 1000))); } + + @Test + public void testCalculateChargeEnergyInChargeGrid() { + assertEquals(1375, calculateChargeEnergyInChargeGrid(// + new GlobalSimulationsContext(CLOCK, TIME, ImmutableList.of(), // + new GlobalSimulationsContext.Grid(0, 20000), // + new GlobalSimulationsContext.Ess(0, 12223, 5000, 5000), // + ImmutableList.of()))); + + assertEquals(525, calculateChargeEnergyInChargeGrid(// + new GlobalSimulationsContext(CLOCK, TIME, ImmutableList.of(), // + new GlobalSimulationsContext.Grid(0, 20000), // + new GlobalSimulationsContext.Ess(0, 12223, 5000, 5000), // + ImmutableList.of(// + new GlobalSimulationsContext.Period.Quarter(TIME, 0, 1000, 0), // + new GlobalSimulationsContext.Period.Quarter(TIME, 100, 1100, 0), // + new GlobalSimulationsContext.Period.Quarter(TIME, 200, 0, 0) // + )))); + + assertEquals(538, calculateChargeEnergyInChargeGrid(// + new GlobalSimulationsContext(CLOCK, TIME, ImmutableList.of(), // + new GlobalSimulationsContext.Grid(0, 20000), // + new GlobalSimulationsContext.Ess(0, 12223, 5000, 5000), // + ImmutableList.of(// + new GlobalSimulationsContext.Period.Quarter(TIME, 0, 700, 123), // + new GlobalSimulationsContext.Period.Quarter(TIME, 100, 600, 123), // + new GlobalSimulationsContext.Period.Quarter(TIME, 200, 500, 125), // + new GlobalSimulationsContext.Period.Quarter(TIME, 300, 400, 126), // + new GlobalSimulationsContext.Period.Quarter(TIME, 400, 300, 123), // + new GlobalSimulationsContext.Period.Quarter(TIME, 500, 200, 122), // + new GlobalSimulationsContext.Period.Quarter(TIME, 600, 100, 121), // + new GlobalSimulationsContext.Period.Quarter(TIME, 700, 0, 121) // + )))); + + assertEquals(499, calculateChargeEnergyInChargeGrid(// + new GlobalSimulationsContext(CLOCK, TIME, ImmutableList.of(), // + new GlobalSimulationsContext.Grid(0, 20000), // + new GlobalSimulationsContext.Ess(0, 12223, 5000, 5000), // + ImmutableList.of(// + new GlobalSimulationsContext.Period.Quarter(TIME, 0, 700, 120), // + new GlobalSimulationsContext.Period.Quarter(TIME, 100, 600, 121), // + new GlobalSimulationsContext.Period.Quarter(TIME, 200, 500, 122), // + new GlobalSimulationsContext.Period.Quarter(TIME, 300, 1140, 126), // + new GlobalSimulationsContext.Period.Quarter(TIME, 400, 1150, 125), // + new GlobalSimulationsContext.Period.Quarter(TIME, 500, 200, 122), // + new GlobalSimulationsContext.Period.Quarter(TIME, 600, 100, 121), // + new GlobalSimulationsContext.Period.Quarter(TIME, 700, 0, 121) // + )))); + } + } diff --git a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponseTest.java b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponseTest.java new file mode 100644 index 00000000000..4372db782f4 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponseTest.java @@ -0,0 +1,242 @@ +package io.openems.edge.controller.ess.timeofusetariff.jsonrpc; + +import static io.openems.common.utils.DateUtils.roundDownToQuarter; +import static io.openems.common.utils.JsonUtils.getAsJsonArray; +import static io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.applyBalancing; +import static io.openems.edge.controller.ess.timeofusetariff.UtilsTest.CLOCK; +import static io.openems.edge.controller.ess.timeofusetariff.jsonrpc.TestData.CONSUMPTION_PREDICTION_QUARTERLY; +import static io.openems.edge.controller.ess.timeofusetariff.jsonrpc.TestData.PAST_HOURLY_PRICES; +import static io.openems.edge.controller.ess.timeofusetariff.jsonrpc.TestData.PAST_SOC; +import static io.openems.edge.controller.ess.timeofusetariff.jsonrpc.TestData.PAST_STATES; +import static io.openems.edge.controller.ess.timeofusetariff.jsonrpc.TestData.PRODUCTION_888_20231106; +import static io.openems.edge.controller.ess.timeofusetariff.jsonrpc.TestData.PRODUCTION_PREDICTION_QUARTERLY; +import static org.junit.Assert.assertEquals; + +import java.time.ZonedDateTime; + +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedMap; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.ChannelAddress; +import io.openems.common.utils.JsonUtils; +import io.openems.common.utils.UuidUtils; +import io.openems.edge.controller.ess.timeofusetariff.StateMachine; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImplTest; +import io.openems.edge.controller.ess.timeofusetariff.Utils; +import io.openems.edge.energy.api.EnergyScheduleHandler.AbstractEnergyScheduleHandler; +import io.openems.edge.energy.api.EnergyScheduleHandler.WithDifferentStates.Period; +import io.openems.edge.energy.api.simulation.EnergyFlow; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.timedata.test.DummyTimedata; + +public class GetScheduleResponseTest { + + @Test + public void test() throws Exception { + final var now = roundDownToQuarter(ZonedDateTime.now(CLOCK)); + final var ess = new DummyManagedSymmetricEss("ess0") // + .withCapacity(10000); + final var model = new EnergyFlow.Model(// + /* production */ 2500, // + /* consumption */ 500, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 0, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyBalancing(model); + final var energyFlow = model.solve(); + + // Simulate historic data + final var timedata = new DummyTimedata("timedata0"); + final var fromDate = now.minusHours(3); + for (var i = 0; i < 12; i++) { + var quarter = fromDate.plusMinutes(i * 15); + timedata.add(quarter, new ChannelAddress("ctrl0", "QuarterlyPrices"), PAST_HOURLY_PRICES[i]); + timedata.add(quarter, new ChannelAddress("ctrl0", "StateMachine"), PAST_STATES[i]); + timedata.add(quarter, Utils.SUM_PRODUCTION, PRODUCTION_PREDICTION_QUARTERLY[i]); + timedata.add(quarter, Utils.SUM_CONSUMPTION, CONSUMPTION_PREDICTION_QUARTERLY[i]); + timedata.add(quarter, Utils.SUM_ESS_SOC, PAST_SOC[i]); + timedata.add(quarter, Utils.SUM_ESS_DISCHARGE_POWER, PRODUCTION_888_20231106[i]); + timedata.add(quarter, Utils.SUM_GRID, PRODUCTION_888_20231106[i]); + } + + // Simulate future Schedule + var ctrl = TimeOfUseTariffControllerImplTest.create(CLOCK, ess, timedata); + var esh = TimeOfUseTariffControllerImplTest.getEnergyScheduleHandler(ctrl); + ((AbstractEnergyScheduleHandler /* this is safe */) esh).initialize(new GlobalSimulationsContext(CLOCK, null, + null, null, new GlobalSimulationsContext.Ess(0, 0, 0, 0), ImmutableList.of())); + esh.applySchedule(ImmutableSortedMap.naturalOrder() // + .put(now.plusMinutes(0), new Period.Transition(1, 0.1, energyFlow, 5000)) // + .put(now.plusMinutes(15), new Period.Transition(0, 0.2, energyFlow, 6000)) // + .put(now.plusMinutes(30), new Period.Transition(0, 0.3, energyFlow, 7000)) // + .build()); + + final var gsr = GetScheduleResponse.from(UuidUtils.getNilUuid(), "ctrl0", CLOCK, ess, timedata, esh); + + var schedule = getAsJsonArray(gsr.getResult(), "schedule"); + + assertEquals(""" + [ + { + "timestamp": "1999-12-31T21:00:00Z", + "price": 158.0, + "state": 1, + "grid": 0, + "production": 0, + "consumption": 1021, + "ess": 0, + "soc": 60 + }, + { + "timestamp": "1999-12-31T21:15:00Z", + "price": 160.0, + "state": 1, + "grid": 0, + "production": 0, + "consumption": 1208, + "ess": 0, + "soc": 62 + }, + { + "timestamp": "1999-12-31T21:30:00Z", + "price": 171.0, + "state": 1, + "grid": 0, + "production": 0, + "consumption": 713, + "ess": 0, + "soc": 64 + }, + { + "timestamp": "1999-12-31T21:45:00Z", + "price": 174.0, + "state": 1, + "grid": 0, + "production": 0, + "consumption": 931, + "ess": 0, + "soc": 66 + }, + { + "timestamp": "1999-12-31T22:00:00Z", + "price": 161.0, + "state": 1, + "grid": 0, + "production": 0, + "consumption": 2847, + "ess": 0, + "soc": 65 + }, + { + "timestamp": "1999-12-31T22:15:00Z", + "price": 152.0, + "state": 3, + "grid": 0, + "production": 0, + "consumption": 2551, + "ess": 0, + "soc": 67 + }, + { + "timestamp": "1999-12-31T22:30:00Z", + "price": 120.0, + "state": 3, + "grid": 0, + "production": 0, + "consumption": 1558, + "ess": 0, + "soc": 70 + }, + { + "timestamp": "1999-12-31T22:45:00Z", + "price": 111.0, + "state": 1, + "grid": 0, + "production": 0, + "consumption": 1234, + "ess": 0, + "soc": 73 + }, + { + "timestamp": "1999-12-31T23:00:00Z", + "price": 105.0, + "state": 2, + "grid": 0, + "production": 0, + "consumption": 433, + "ess": 0, + "soc": 76 + }, + { + "timestamp": "1999-12-31T23:15:00Z", + "price": 105.0, + "state": 1, + "grid": 0, + "production": 0, + "consumption": 633, + "ess": 0, + "soc": 79 + }, + { + "timestamp": "1999-12-31T23:30:00Z", + "price": 74.0, + "state": 2, + "grid": 0, + "production": 0, + "consumption": 1355, + "ess": 0, + "soc": 83 + }, + { + "timestamp": "1999-12-31T23:45:00Z", + "price": 73.0, + "state": 2, + "grid": 0, + "production": 0, + "consumption": 606, + "ess": 0, + "soc": 87 + }, + { + "timestamp": "2000-01-01T00:00:00Z", + "price": 0.1, + "state": 0, + "grid": 0, + "production": 10000, + "consumption": 2000, + "ess": -8000, + "soc": 50 + }, + { + "timestamp": "2000-01-01T00:15:00Z", + "price": 0.2, + "state": 1, + "grid": 0, + "production": 10000, + "consumption": 2000, + "ess": -8000, + "soc": 60 + }, + { + "timestamp": "2000-01-01T00:30:00Z", + "price": 0.3, + "state": 1, + "grid": 0, + "production": 10000, + "consumption": 2000, + "ess": -8000, + "soc": 70 + } + ]""", JsonUtils.prettyToString(schedule)); + } + + @Test + public void testEmpty() throws OpenemsNamedException { + var response = GetScheduleResponse.empty(CLOCK, StateMachine.BALANCING).toList().get(0); + assertEquals(StateMachine.BALANCING.getValue(), JsonUtils.getAsInt(response, "state")); + } +} diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/TestData.java b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/TestData.java similarity index 61% rename from io.openems.edge.energy/test/io/openems/edge/energy/TestData.java rename to io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/TestData.java index c89873819e7..a56e5a5097d 100644 --- a/io.openems.edge.energy/test/io/openems/edge/energy/TestData.java +++ b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/TestData.java @@ -1,31 +1,26 @@ -package io.openems.edge.energy; - -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; - -import io.openems.edge.controller.ess.timeofusetariff.StateMachine; +package io.openems.edge.controller.ess.timeofusetariff.jsonrpc; public class TestData { // Edge 888; 06.11.2023 - public static final Integer[] PRODUCTION_888_20231106 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 12, 19, 24, 92, 301, 441, 653, 741, 1921, 1923, 1649, 2045, 2638, 3399, 4071, 4359, + protected static final Integer[] PRODUCTION_888_20231106 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 12, 19, 24, 92, 301, 441, 653, 741, 1921, 1923, 1649, 2045, 2638, 3399, 4071, 4359, 4516, 5541, 6993, 6292, 3902, 7700, 9098, 9555, 8119, 6868, 6560, 6380, 6193, 5389, 4349, 3743, 5367, 5319, 4383, 2243, 1122, 1315, 1107, 268, 48, 2, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - public static final Integer[] CONSUMPTION_888_20231106 = { 308, 285, 384, 471, 480, 385, 464, 448, 288, 333, 346, + protected static final Integer[] CONSUMPTION_888_20231106 = { 308, 285, 384, 471, 480, 385, 464, 448, 288, 333, 346, 313, 1786, 332, 300, 259, 373, 358, 279, 308, 309, 415, 392, 299, 2913, 3105, 4416, 4442, 497, 5910, 4106, 2171, 3898, 922, 1601, 1088, 303, 2384, 430, 2428, 2899, 371, 613, 1663, 366, 2072, 456, 1589, 2004, 488, 199, 1628, 613, 198, 1796, 202, 1180, 4975, 4493, 5511, 7757, 2926, 2640, 4335, 2630, 2799, 5111, 2979, 3062, 4842, 4194, 4474, 4750, 4876, 1238, 1395, 1425, 1123, 3366, 4088, 418, 436, 3234, 1504, 1092, 1853, 365, 628, 2095, 552, 1113, 1808, 3223, 1629, 1329, 264 }; - public static final Double[] PRICES_888_20231106 = { 155., 152., 152., 152., 157., 172., 238., 266., 266., 241., + protected static final Double[] PRICES_888_20231106 = { 155., 152., 152., 152., 157., 172., 238., 266., 266., 241., 224., 219., 221., 232., 248., 271., 286., 316., 332., 318., 284., 278., 270., 257. }; - public static final Integer[] PRODUCTION_PREDICTION_QUARTERLY = { + protected static final Integer[] PRODUCTION_PREDICTION_QUARTERLY = { /* 00:00-03:45 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // /* 04:00-07:45 */ @@ -52,8 +47,7 @@ public class TestData { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // }; - public static final Integer[] CONSUMPTION_PREDICTION_QUARTERLY = { - + protected static final Integer[] CONSUMPTION_PREDICTION_QUARTERLY = { /* 00:00-03:450 */ 1021, 1208, 713, 931, 2847, 2551, 1558, 1234, 433, 633, 1355, 606, 430, 1432, 1121, 502, // /* 04:00-07:45 */ @@ -80,7 +74,7 @@ public class TestData { 3226, 2358, 1778, 1002, 455, 654, 534, 1587, 1638, 459, 330, 258, 368, 728, 1096, 878 // }; - public static final Double[] HOURLY_PRICES_SUMMER = { // + protected static final Double[] HOURLY_PRICES_SUMMER = { // 70.95, 71.98, 71.95, 74.96, // 78.93, 80., 84.01, 111.03, // 105.04, 105., 74.23, 73.28, // @@ -89,35 +83,19 @@ public class TestData { 149.99, 157.43, 130.9, 120.14 // }; - public static final StateMachine[] STATES = { // - BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, DELAY_DISCHARGE, - BALANCING, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, - DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, - DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, - DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, - BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, - BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, - BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, - BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, - BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, BALANCING, - BALANCING, BALANCING, BALANCING, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, - DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE, - DELAY_DISCHARGE, DELAY_DISCHARGE, DELAY_DISCHARGE // - }; - - public static final Integer[] PAST_STATES = { // + protected static final Integer[] PAST_STATES = { // 1, 1, 1, 1, // 1, 3, 3, 1, // 2, 1, 2, 2, // }; - public static final Integer[] PAST_SOC = { // + protected static final Integer[] PAST_SOC = { // 60, 62, 64, 66, // 65, 67, 70, 73, // 76, 79, 83, 87, // }; - public static final Integer[] PAST_HOURLY_PRICES = { // + protected static final Integer[] PAST_HOURLY_PRICES = { // 158, 160, 171, 174, // 161, 152, 120, 111, // 105, 105, 74, 73, // diff --git a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/v1/UtilsV1Test.java b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/v1/UtilsV1Test.java new file mode 100644 index 00000000000..cd5fd7bd569 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/v1/UtilsV1Test.java @@ -0,0 +1,101 @@ +package io.openems.edge.controller.ess.timeofusetariff.v1; + +import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; +import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.CHARGE_GRID; +import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; +import static io.openems.edge.controller.ess.timeofusetariff.v1.UtilsV1.calculateAutomaticMode; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.openems.edge.common.sum.DummySum; +import io.openems.edge.controller.ess.timeofusetariff.Utils.ApplyState; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; + +@SuppressWarnings("deprecation") +public class UtilsV1Test { + + @Test + public void testCalculateAutomaticMode() { + assertEquals("Null-Check", new ApplyState(BALANCING, null), // + calculateAutomaticMode(// + new DummySum(), // + new DummyManagedSymmetricEss("ess0"), // + /* essChargeInChargeGrid */ 1000, // + /* maxChargePowerFromGrid */ 2000, // + /* limitChargePowerFor14aEnWG */ true, // + BALANCING)); + assertEquals("Null-Check", new ApplyState(BALANCING, null), // + calculateAutomaticMode(// + new DummySum() // + .withGridActivePower(100), // + new DummyManagedSymmetricEss("ess0"), // + /* essChargeInChargeGrid */ 1000, // + /* maxChargePowerFromGrid */ 2000, // + /* limitChargePowerFor14aEnWG */ true, // + BALANCING)); + + assertEquals("BALANCING", new ApplyState(BALANCING, null), // + calculateAutomaticMode(// + new DummySum() // + .withGridActivePower(100), // + new DummyManagedSymmetricEss("ess0") // + .withActivePower(500), // + /* essChargeInChargeGrid */ 1000, // + /* maxChargePowerFromGrid */ 2000, // + /* limitChargePowerFor14aEnWG */ true, // + BALANCING)); + + assertEquals("DELAY_DISCHARGE stays DELAY_DISCHARGE", new ApplyState(DELAY_DISCHARGE, 0), // + calculateAutomaticMode(// + new DummySum() // + .withGridActivePower(100), // + new DummyManagedSymmetricEss("ess0") // + .withActivePower(500), // + /* essChargeInChargeGrid */ 1000, // + /* maxChargePowerFromGrid */ 2000, // + /* limitChargePowerFor14aEnWG */ true, // + DELAY_DISCHARGE)); + assertEquals("DELAY_DISCHARGE to BALANCING", new ApplyState(BALANCING, null), // + calculateAutomaticMode(// + new DummySum() // + .withGridActivePower(-500), // + new DummyManagedSymmetricEss("ess0") // + .withActivePower(500), // + /* essChargeInChargeGrid */ 1000, // + /* maxChargePowerFromGrid */ 2000, // + /* limitChargePowerFor14aEnWG */ true, // + DELAY_DISCHARGE)); + + assertEquals("CHARGE_GRID stays CHARGE_GRID", new ApplyState(CHARGE_GRID, -1400), // + calculateAutomaticMode(// + new DummySum() // + .withGridActivePower(100), // + new DummyManagedSymmetricEss("ess0") // + .withActivePower(500), // + /* essChargeInChargeGrid */ 1000, // + /* maxChargePowerFromGrid */ 2000, // + /* limitChargePowerFor14aEnWG */ true, // + CHARGE_GRID)); + assertEquals("CHARGE_GRID to DELAY_DISCHARGE", new ApplyState(DELAY_DISCHARGE, 0), // + calculateAutomaticMode(// + new DummySum() // + .withGridActivePower(100), // + new DummyManagedSymmetricEss("ess0") // + .withActivePower(500), // + /* essChargeInChargeGrid */ 1000, // + /* maxChargePowerFromGrid */ 400, // + /* limitChargePowerFor14aEnWG */ true, // + CHARGE_GRID)); + assertEquals("CHARGE_GRID to BALANCING", new ApplyState(BALANCING, null), // + calculateAutomaticMode(// + new DummySum() // + .withGridActivePower(-500), // + new DummyManagedSymmetricEss("ess0") // + .withActivePower(500), // + /* essChargeInChargeGrid */ 1000, // + /* maxChargePowerFromGrid */ 0, // + /* limitChargePowerFor14aEnWG */ true, // + CHARGE_GRID)); + } +} diff --git a/io.openems.edge.controller.evcs.fixactivepower/bnd.bnd b/io.openems.edge.controller.evcs.fixactivepower/bnd.bnd index 98e4180dcd7..ba83284def5 100644 --- a/io.openems.edge.controller.evcs.fixactivepower/bnd.bnd +++ b/io.openems.edge.controller.evcs.fixactivepower/bnd.bnd @@ -8,7 +8,8 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.common,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ - io.openems.edge.evcs.api + io.openems.edge.evcs.api,\ + io.openems.edge.meter.api,\ -testpath: \ ${testpath} diff --git a/io.openems.edge.controller.evcs.fixactivepower/test/io/openems/edge/controller/evcs/fixactivepower/ControllerEvcsFixActivePowerImplTest.java b/io.openems.edge.controller.evcs.fixactivepower/test/io/openems/edge/controller/evcs/fixactivepower/ControllerEvcsFixActivePowerImplTest.java index e3f8ed55368..85c46e49269 100644 --- a/io.openems.edge.controller.evcs.fixactivepower/test/io/openems/edge/controller/evcs/fixactivepower/ControllerEvcsFixActivePowerImplTest.java +++ b/io.openems.edge.controller.evcs.fixactivepower/test/io/openems/edge/controller/evcs/fixactivepower/ControllerEvcsFixActivePowerImplTest.java @@ -7,19 +7,16 @@ public class ControllerEvcsFixActivePowerImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String EVCS_ID = "evcs0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerEvcsFixActivePowerImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEvcsId(EVCS_ID) // + .setId("ctrl0") // + .setEvcsId("evcs0") // .setPower(0) // .setUpdateFrequency(1) // - .build()); // - ; + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.evcs/bnd.bnd b/io.openems.edge.controller.evcs/bnd.bnd index c3a69ba9f20..e0be766da07 100644 --- a/io.openems.edge.controller.evcs/bnd.bnd +++ b/io.openems.edge.controller.evcs/bnd.bnd @@ -9,7 +9,8 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.common,\ io.openems.edge.controller.api,\ io.openems.edge.ess.api,\ - io.openems.edge.evcs.api + io.openems.edge.evcs.api,\ + io.openems.edge.meter.api,\ -testpath: \ ${testpath} diff --git a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ChargingLowerThanTargetHandler.java b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ChargingLowerThanTargetHandler.java index 418c50c5e0d..f052d2615bd 100644 --- a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ChargingLowerThanTargetHandler.java +++ b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ChargingLowerThanTargetHandler.java @@ -64,7 +64,7 @@ protected boolean isLower(ManagedEvcs evcs) throws InvalidValueException { * @throws InvalidValueException invalidValueException */ protected boolean isChargingLowerThanTarget(ManagedEvcs evcs) throws InvalidValueException { - int chargePower = evcs.getChargePower().orElse(0); + int chargePower = evcs.getActivePower().orElse(0); int chargePowerTarget = evcs.getSetChargePowerLimit().orElse(evcs.getMaximumHardwarePower().getOrError()); if (chargePowerTarget - chargePower > chargePowerTarget * CHARGING_TARGET_MAX_DIFFERENCE_PERCENT) { diff --git a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/Config.java b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/Config.java index bfda174afa0..9edecbc6394 100644 --- a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/Config.java +++ b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/Config.java @@ -43,6 +43,12 @@ @AttributeDefinition(name = "Energy limit in this session in [Wh]", description = "Set the Energylimit in this Session in Wh. The charging station will only charge till this limit; '0' is no limit.") int energySessionLimit() default 0; + @AttributeDefinition(name = "Minimum charging time while charging with excess power", description = "Minimum time (Seconds) is applied to avoid continuous switching between charging and not charging") + int excessChargeHystersis() default 120; + + @AttributeDefinition(name = "Minimum pause time while charging with excess power", description = "Minimum time (Seconds) is applied to avoid continuous switching between charging and not charging") + int excessChargePauseHysteresis() default 30; + @AttributeDefinition(name = "Evcs target filter", description = "This is auto-generated by 'Evcs-ID'.") String evcs_target() default "(enabled=true)"; diff --git a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ControllerEvcs.java b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ControllerEvcs.java index 5a4a779dd4f..54bbb5e0be5 100644 --- a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ControllerEvcs.java +++ b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ControllerEvcs.java @@ -1,13 +1,17 @@ package io.openems.edge.controller.evcs; -import io.openems.common.types.OpenemsType; +import static io.openems.common.channel.PersistencePriority.HIGH; +import static io.openems.common.types.OpenemsType.BOOLEAN; + import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; -public interface ControllerEvcs { +public interface ControllerEvcs extends OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - AWAITING_HYSTERESIS(Doc.of(OpenemsType.BOOLEAN)) // + AWAITING_HYSTERESIS(Doc.of(BOOLEAN) // + .persistencePriority(HIGH)) // ; // private final Doc doc; diff --git a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ControllerEvcsImpl.java b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ControllerEvcsImpl.java index a4aeb209438..4a1d1dd5653 100644 --- a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ControllerEvcsImpl.java +++ b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ControllerEvcsImpl.java @@ -2,6 +2,8 @@ import java.io.IOException; import java.time.Clock; +import java.time.Duration; +import java.time.Instant; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; @@ -25,6 +27,8 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.sum.Sum; import io.openems.edge.controller.api.Controller; +import io.openems.edge.evcs.api.ChargeMode; +import io.openems.edge.evcs.api.ChargeState; import io.openems.edge.evcs.api.ManagedEvcs; @Designate(ocd = Config.class, factory = true) @@ -40,6 +44,16 @@ public class ControllerEvcsImpl extends AbstractOpenemsComponent implements Cont private final Logger log = LoggerFactory.getLogger(ControllerEvcsImpl.class); private final ChargingLowerThanTargetHandler chargingLowerThanTargetHandler; + private final Clock clock; + + // Time of last charge power change, used for the hysteresis + private Instant lastInitialCharge = Instant.MIN; + + // Time of last charge pause, used for the hysteresis + private Instant lastChargePause = Instant.MIN; + + // Last charge power, used for the hysteresis + private int lastChargePower = 0; @Reference private ConfigurationAdmin cm; @@ -62,6 +76,7 @@ protected ControllerEvcsImpl(Clock clock) { Controller.ChannelId.values(), // ControllerEvcs.ChannelId.values() // ); + this.clock = clock; this.chargingLowerThanTargetHandler = new ChargingLowerThanTargetHandler(clock); } @@ -178,7 +193,7 @@ public void run() throws OpenemsNamedException { */ if (nextChargePower != 0) { - int chargePower = this.evcs.getChargePower().orElse(0); + int activePower = this.evcs.getActivePower().orElse(0); /** * Check the difference of the current charge power and the previous charging @@ -196,15 +211,20 @@ public void run() throws OpenemsNamedException { int currMax = this.evcs.getMaximumPower().orElse(0); /** - * If the charge power would increases again above the current maximum power, it - * resets the maximum Power. + * If the power would increases again above the current maximum power, it resets + * the maximum Power. */ - if (chargePower > currMax * (1 + DEFAULT_UPPER_TARGET_DIFFERENCE_PERCENT)) { + if (activePower > currMax * (1 + DEFAULT_UPPER_TARGET_DIFFERENCE_PERCENT)) { this.evcs._setMaximumPower(null); } } } + if (this.config.chargeMode().equals(ChargeMode.EXCESS_POWER)) { + // Apply hysteresis + nextChargePower = this.applyHysteresis(nextChargePower); + } + if (isClustered) { this.evcs.setChargePowerRequest(nextChargePower); } else { @@ -251,7 +271,7 @@ private void adaptConfigToHardwareLimits() { private static int calculateChargePowerFromExcessPower(Sum sum, ManagedEvcs evcs) throws OpenemsNamedException { int buyFromGrid = sum.getGridActivePower().orElse(0); int essDischarge = sum.getEssDischargePower().orElse(0); - int evcsCharge = evcs.getChargePower().orElse(0); + int evcsCharge = evcs.getActivePower().orElse(0); return evcsCharge - buyFromGrid - essDischarge; } @@ -265,7 +285,7 @@ private static int calculateChargePowerFromExcessPower(Sum sum, ManagedEvcs evcs */ private static int calculateExcessPowerAfterEss(Sum sum, ManagedEvcs evcs) { int buyFromGrid = sum.getGridActivePower().orElse(0); - int evcsCharge = evcs.getChargePower().orElse(0); + int evcsCharge = evcs.getActivePower().orElse(0); var result = evcsCharge - buyFromGrid; @@ -275,6 +295,82 @@ private static int calculateExcessPowerAfterEss(Sum sum, ManagedEvcs evcs) { return result > 0 ? result : 0; } + /** + * Applies the hysteresis to avoid too quick changes between a charge process + * and a pause. + * + * @param nextChargePower the next charge power limit + * @return next charge power or the last power if hysteresis is active + */ + private int applyHysteresis(int nextChargePower) { + int targetChargePower = nextChargePower; + boolean showWarning = false; + var now = Instant.now(this.clock); + + // Wait at least the EVCS-specific response time, required to increase and + // decrease the charging power + if (awaitLastChanges(this.evcs.getChargeState().asEnum())) { + // Still waiting for increasing, decreasing the power or undefined + return this.lastChargePower; + } + // TODO: Show info, test and check if bellow logic still needed or need to be + // different (Change only when we would change for xSeconds) + + // New charge power limit + if (this.lastChargePower <= 0 && nextChargePower > 0) { + var hysteresis = Duration.ofSeconds(this.config.excessChargePauseHysteresis()); + if (this.lastChargePause.plus(hysteresis).isBefore(now)) { + + // Start charing + this.lastInitialCharge = now; + } else { + // Wait for hysteresis + showWarning = true; + targetChargePower = this.lastChargePower; + } + } + + // Pause charging by limiting to zero + if (this.lastChargePower > 0 && nextChargePower <= 0) { + var hysteresis = Duration.ofSeconds(this.config.excessChargeHystersis()); + if (this.lastInitialCharge.plus(hysteresis).isBefore(now)) { + + // Pause charing + targetChargePower = 0; + this.lastChargePause = now; + } else { + // Wait for hysteresis + showWarning = true; + targetChargePower = this.lastChargePower; + } + } + + // Apply results + this.lastChargePower = targetChargePower; + this.channel(ControllerEvcs.ChannelId.AWAITING_HYSTERESIS).setNextValue(showWarning); + + return targetChargePower; + } + + /** + * Check if the evcs should wait for last changes. + * + *

+ * Since the charging stations and each car have their own response time until + * they charge at the set power, the controller waits until everything runs + * normally. + * + * @param chargeState current evcs charge state + * @return The cvcs should await or not + */ + private static boolean awaitLastChanges(ChargeState chargeState) { + if (chargeState.equals(ChargeState.INCREASING) || chargeState.equals(ChargeState.INCREASING)) { + // Still waiting for increasing, decreasing the power + return true; + } + return false; + } + @Override public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { return new ModbusSlaveTable(// diff --git a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java index 1c4f620d146..e51964f0f59 100644 --- a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java +++ b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java @@ -1,168 +1,156 @@ package io.openems.edge.controller.evcs; +import static io.openems.edge.common.sum.Sum.ChannelId.ESS_DISCHARGE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.ESS_SOC; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_ACTIVE_POWER; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.controller.evcs.ControllerEvcs.ChannelId.AWAITING_HYSTERESIS; +import static io.openems.edge.controller.evcs.Priority.CAR; +import static io.openems.edge.evcs.api.ChargeMode.EXCESS_POWER; +import static io.openems.edge.evcs.api.ChargeMode.FORCE_CHARGE; +import static io.openems.edge.evcs.api.Evcs.ChannelId.MAXIMUM_HARDWARE_POWER; +import static io.openems.edge.evcs.api.Evcs.ChannelId.MAXIMUM_POWER; +import static io.openems.edge.evcs.api.Evcs.ChannelId.STATUS; +import static io.openems.edge.evcs.api.ManagedEvcs.ChannelId.IS_CLUSTERED; +import static io.openems.edge.evcs.api.ManagedEvcs.ChannelId.SET_CHARGE_POWER_LIMIT; +import static io.openems.edge.evcs.api.ManagedEvcs.ChannelId.SET_CHARGE_POWER_REQUEST; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER; +import static java.time.temporal.ChronoUnit.MINUTES; import static org.junit.Assert.assertEquals; -import java.time.Instant; -import java.time.ZoneOffset; - import org.junit.Test; -import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; -import io.openems.edge.common.filter.DisabledRampFilter; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.controller.test.ControllerTest; -import io.openems.edge.evcs.api.ChargeMode; +import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.Status; -import io.openems.edge.evcs.test.DummyEvcsPower; import io.openems.edge.evcs.test.DummyManagedEvcs; public class ControllerEvcsImplTest { - private static final DummyEvcsPower EVCS_POWER = new DummyEvcsPower(new DisabledRampFilter()); - private static final DummyManagedEvcs EVCS = new DummyManagedEvcs("evcs0", EVCS_POWER); - - private static final String EVCS_ID = EVCS.id(); - private static final boolean DEFAULT_ENABLE_CHARGING = true; - private static final ChargeMode DEFAULT_CHARGE_MODE = ChargeMode.EXCESS_POWER; private static final int DEFAULT_FORCE_CHARGE_MIN_POWER = 7360; private static final int DEFAULT_CHARGE_MIN_POWER = 0; - private static final Priority DEFAULT_PRIORITY = Priority.CAR; - private static final int DEFAULT_ENERGY_SESSION_LIMIT = 0; - - private static ChannelAddress sumGridActivePower = new ChannelAddress("_sum", "GridActivePower"); - private static ChannelAddress sumEssDischargePower = new ChannelAddress("_sum", "EssDischargePower"); - private static ChannelAddress sumEssSoc = new ChannelAddress("_sum", "EssSoc"); - private static ChannelAddress evcs0ChargePower = new ChannelAddress("evcs0", "ChargePower"); - private static ChannelAddress evcs0SetChargePowerLimit = new ChannelAddress("evcs0", "SetChargePowerLimit"); - private static ChannelAddress evcs0MaximumPower = new ChannelAddress("evcs0", "MaximumPower"); - private static ChannelAddress evcs0IsClustered = new ChannelAddress("evcs0", "IsClustered"); - private static ChannelAddress evcs0SetPowerRequest = new ChannelAddress("evcs0", "SetChargePowerRequest"); - private static ChannelAddress evcs0Status = new ChannelAddress("evcs0", "Status"); - private static ChannelAddress evcs0MaximumHardwarePower = new ChannelAddress("evcs0", "MaximumHardwarePower"); @Test public void excessChargeTest1() throws Exception { - new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // - .setEvcsId(EVCS_ID) // - .setEnableCharging(DEFAULT_ENABLE_CHARGING) // - .setChargeMode(DEFAULT_CHARGE_MODE) // + .setEvcsId("evcs0") // + .setEnableCharging(true) // + .setChargeMode(EXCESS_POWER) // .setForceChargeMinPower(DEFAULT_FORCE_CHARGE_MIN_POWER) // .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // - .setPriority(DEFAULT_PRIORITY) // - .setEnergySessionLimit(DEFAULT_ENERGY_SESSION_LIMIT) // + .setPriority(CAR) // + .setEnergySessionLimit(0) // .build()) // .next(new TestCase() // - .input(sumEssDischargePower, -6000) // - .input(evcs0IsClustered, false) // - .input(sumGridActivePower, 0) // - .input(evcs0ChargePower, 0) // - .output(evcs0SetChargePowerLimit, 6000)) // - ; + .input(ESS_DISCHARGE_POWER, -6000) // + .input("evcs0", IS_CLUSTERED, false) // + .input(GRID_ACTIVE_POWER, 0) // + .input("evcs0", ACTIVE_POWER, 0) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 6000)) // + .deactivate(); } @Test public void excessChargeTest2() throws Exception { - - final var test = new ControllerTest(new ControllerEvcsImpl()) // + new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // - .setEvcsId(EVCS_ID) // - .setEnableCharging(DEFAULT_ENABLE_CHARGING) // - .setChargeMode(DEFAULT_CHARGE_MODE) // + .setEvcsId("evcs0") // + .setEnableCharging(true) // + .setChargeMode(EXCESS_POWER) // .setForceChargeMinPower(DEFAULT_FORCE_CHARGE_MIN_POWER) // .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // .setPriority(Priority.STORAGE) // - .setEnergySessionLimit(DEFAULT_ENERGY_SESSION_LIMIT) // - .build()); // - - test.next(new TestCase() // - .input(sumEssSoc, 50) // - .input(sumEssDischargePower, -5000) // - .input(evcs0IsClustered, false) // - .input(sumGridActivePower, -40000) // - .input(evcs0ChargePower, 5000) // - .input(evcs0MaximumHardwarePower, 22080) // - .output(evcs0SetChargePowerLimit, 44800)); + .setEnergySessionLimit(0) // + .build()) // + .next(new TestCase() // + .input(ESS_SOC, 50) // + .input(ESS_DISCHARGE_POWER, -5000) // + .input("evcs0", IS_CLUSTERED, false) // + .input(GRID_ACTIVE_POWER, -40000) // + .input("evcs0", ACTIVE_POWER, 5000) // + .input("evcs0", MAXIMUM_HARDWARE_POWER, 22080) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 44800)) // + .deactivate(); } @Test public void forceChargeTest() throws Exception { - new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // - .setEvcsId(EVCS_ID) // - .setEnableCharging(DEFAULT_ENABLE_CHARGING) // - .setChargeMode(ChargeMode.FORCE_CHARGE) // + .setEvcsId("evcs0") // + .setEnableCharging(true) // + .setChargeMode(FORCE_CHARGE) // .setForceChargeMinPower(DEFAULT_FORCE_CHARGE_MIN_POWER) // .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // - .setPriority(DEFAULT_PRIORITY) // - .setEnergySessionLimit(DEFAULT_ENERGY_SESSION_LIMIT) // + .setPriority(CAR) // s + .setEnergySessionLimit(0) // .build()) // .next(new TestCase() // - .input(sumEssDischargePower, -5000) // - .input(evcs0IsClustered, false) // - .input(sumGridActivePower, -40000) // - .input(evcs0ChargePower, 5000) // - .output(evcs0SetChargePowerLimit, 22080)); + .input(ESS_DISCHARGE_POWER, -5000) // + .input("evcs0", IS_CLUSTERED, false) // + .input(GRID_ACTIVE_POWER, -40000) // + .input("evcs0", ACTIVE_POWER, 5000) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 22080)) // + .deactivate(); } @Test public void chargingDisabledTest() throws Exception { - new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // - .setEvcsId(EVCS_ID) // + .setEvcsId("evcs0") // .setEnableCharging(false) // - .setChargeMode(DEFAULT_CHARGE_MODE) // + .setChargeMode(EXCESS_POWER) // .setForceChargeMinPower(DEFAULT_FORCE_CHARGE_MIN_POWER) // .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // - .setPriority(DEFAULT_PRIORITY) // - .setEnergySessionLimit(DEFAULT_ENERGY_SESSION_LIMIT) // + .setPriority(CAR) // + .setEnergySessionLimit(0) // .build()) // .next(new TestCase() // - .output(evcs0SetChargePowerLimit, 0)); + .output("evcs0", SET_CHARGE_POWER_LIMIT, 0)) // + .deactivate(); } @Test public void wrongConfigParametersTest() throws Exception { - var cm = new DummyConfigurationAdmin(); new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", cm) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // - .setEvcsId(EVCS_ID) // - .setEnableCharging(DEFAULT_ENABLE_CHARGING) // - .setChargeMode(DEFAULT_CHARGE_MODE) // + .setEvcsId("evcs0") // + .setEnableCharging(true) // + .setChargeMode(EXCESS_POWER) // .setForceChargeMinPower(30_000) // .setDefaultChargeMinPower(30_000) // - .setPriority(DEFAULT_PRIORITY) // - .setEnergySessionLimit(DEFAULT_ENERGY_SESSION_LIMIT) // + .setPriority(CAR) // + .setEnergySessionLimit(0) // .build()) // .next(new TestCase() // - .input(evcs0MaximumHardwarePower, 12000)); + .input("evcs0", MAXIMUM_HARDWARE_POWER, 12000)) // + .deactivate(); assertEquals(12000, (int) (Integer) cm.getConfiguration("ctrlEvcs0").getProperties().get("defaultChargeMinPower")); @@ -170,101 +158,174 @@ public void wrongConfigParametersTest() throws Exception { @Test public void clusterTest() throws Exception { - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - + final var clock = createDummyClock(); new ControllerTest(new ControllerEvcsImpl(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // - .setEvcsId(EVCS_ID) // - .setEnableCharging(DEFAULT_ENABLE_CHARGING) // - .setChargeMode(DEFAULT_CHARGE_MODE) // + .setEvcsId("evcs0") // + .setEnableCharging(true) // + .setChargeMode(EXCESS_POWER) // .setForceChargeMinPower(3_333) // .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // - .setPriority(DEFAULT_PRIORITY) // - .setEnergySessionLimit(DEFAULT_ENERGY_SESSION_LIMIT) // + .setPriority(CAR) // + .setEnergySessionLimit(0) // .build()) // .next(new TestCase() // - .input(sumEssDischargePower, -10000) // - .input(evcs0IsClustered, true) // - .input(sumGridActivePower, 0) // - .input(evcs0ChargePower, 0) // - .input(evcs0Status, Status.CHARGING) // - .output(evcs0SetPowerRequest, 10000)) + .input(ESS_DISCHARGE_POWER, -10000) // + .input("evcs0", IS_CLUSTERED, true) // + .input(GRID_ACTIVE_POWER, 0) // + .input("evcs0", ACTIVE_POWER, 0) // + .input("evcs0", STATUS, Status.CHARGING) // + .output("evcs0", SET_CHARGE_POWER_REQUEST, 10000)) .next(new TestCase() // - .input(sumEssDischargePower, -6000) // - .input(evcs0IsClustered, true) // - .input(sumGridActivePower, 0) // - .input(evcs0ChargePower, 0) // - .input(evcs0Status, Status.NOT_READY_FOR_CHARGING) // - .output(evcs0SetPowerRequest, 0)) // + .input(ESS_DISCHARGE_POWER, -6000) // + .input("evcs0", IS_CLUSTERED, true) // + .input(GRID_ACTIVE_POWER, 0) // + .input("evcs0", ACTIVE_POWER, 0) // + .input("evcs0", STATUS, Status.NOT_READY_FOR_CHARGING) // + .output("evcs0", SET_CHARGE_POWER_REQUEST, 0)) // f .next(new TestCase() // - .input(sumEssDischargePower, -6000) // - .input(evcs0IsClustered, true) // - .input(sumGridActivePower, 0) // - .input(evcs0ChargePower, 0) // - .input(evcs0Status, null) // - .output(evcs0SetPowerRequest, 0)) // + .input(ESS_DISCHARGE_POWER, -6000) // + .input("evcs0", IS_CLUSTERED, true) // + .input(GRID_ACTIVE_POWER, 0) // + .input("evcs0", ACTIVE_POWER, 0) // + .input("evcs0", STATUS, null) // + .output("evcs0", SET_CHARGE_POWER_REQUEST, 0)) // .next(new TestCase() // - .input(sumEssDischargePower, -6000) // - .input(evcs0IsClustered, true) // - .input(sumGridActivePower, 0) // - .input(evcs0ChargePower, 0) // - .input(evcs0Status, Status.CHARGING_REJECTED) // - .output(evcs0SetPowerRequest, 6000) // - .output(evcs0MaximumPower, null)) // - ; + .input(ESS_DISCHARGE_POWER, -6000) // + .input("evcs0", IS_CLUSTERED, true) // + .input(GRID_ACTIVE_POWER, 0) // + .input("evcs0", ACTIVE_POWER, 0) // + .input("evcs0", STATUS, Status.CHARGING_REJECTED) // + .output("evcs0", SET_CHARGE_POWER_REQUEST, 6000) // + .output("evcs0", MAXIMUM_POWER, null)) // + .deactivate(); } @Test public void clusterTestDisabledCharging() throws Exception { - new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // - .setEvcsId(EVCS_ID) // + .setEvcsId("evcs0") // .setEnableCharging(false) // - .setChargeMode(DEFAULT_CHARGE_MODE) // + .setChargeMode(EXCESS_POWER) // .setForceChargeMinPower(3_333) // .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // - .setPriority(DEFAULT_PRIORITY) // - .setEnergySessionLimit(DEFAULT_ENERGY_SESSION_LIMIT) // + .setPriority(CAR) // + .setEnergySessionLimit(0) // .build()) // .next(new TestCase() // - .input(sumEssDischargePower, -10000) // - .input(evcs0IsClustered, true) // - .input(sumGridActivePower, 0) // - .input(evcs0ChargePower, 0) // - .input(evcs0Status, Status.CHARGING) // - .output(evcs0SetChargePowerLimit, 0)) + .input(ESS_DISCHARGE_POWER, -10000) // + .input("evcs0", IS_CLUSTERED, true) // + .input(GRID_ACTIVE_POWER, 0) // + .input("evcs0", ACTIVE_POWER, 0) // + .input("evcs0", STATUS, Status.CHARGING) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 0)) + .next(new TestCase() // + .input(ESS_DISCHARGE_POWER, -6000) // + .input("evcs0", IS_CLUSTERED, true) // + .input(GRID_ACTIVE_POWER, 0) // + .input("evcs0", ACTIVE_POWER, 0) // + .input("evcs0", STATUS, Status.NOT_READY_FOR_CHARGING) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 0)) // .next(new TestCase() // - .input(sumEssDischargePower, -6000) // - .input(evcs0IsClustered, true) // - .input(sumGridActivePower, 0) // - .input(evcs0ChargePower, 0) // - .input(evcs0Status, Status.NOT_READY_FOR_CHARGING) // - .output(evcs0SetChargePowerLimit, 0)) // + .input(ESS_DISCHARGE_POWER, -6000) // + .input("evcs0", IS_CLUSTERED, true) // + .input(GRID_ACTIVE_POWER, 0) // + .input("evcs0", ACTIVE_POWER, 0) // + .input("evcs0", STATUS, null) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 0)) // .next(new TestCase() // - .input(sumEssDischargePower, -6000) // - .input(evcs0IsClustered, true) // - .input(sumGridActivePower, 0) // - .input(evcs0ChargePower, 0) // - .input(evcs0Status, null) // - .output(evcs0SetChargePowerLimit, 0)) // + .input(ESS_DISCHARGE_POWER, -6000) // + .input("evcs0", IS_CLUSTERED, true) // + .input(GRID_ACTIVE_POWER, 0) // + .input("evcs0", ACTIVE_POWER, 0) // + .input("evcs0", STATUS, Status.CHARGING_REJECTED) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 0) // + .output("evcs0", MAXIMUM_POWER, null)) // + .deactivate(); + } + + @Test + public void hysteresisTest() throws Exception { + final var clock = createDummyClock(); + new ControllerTest(new ControllerEvcsImpl(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("sum", new DummySum()) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // + .activate(MyConfig.create() // + .setId("ctrlEvcs0") // + .setEvcsId("evcs0") // + .setEnableCharging(true) // + .setChargeMode(EXCESS_POWER) // + .setForceChargeMinPower(DEFAULT_FORCE_CHARGE_MIN_POWER) // + .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // + .setPriority(CAR) // + .setEnergySessionLimit(0) // + .setExcessChargeHystersis(120) // + .setExcessChargePauseHysteresis(30) // + .build()) // + .next(new TestCase() // + .input(ESS_DISCHARGE_POWER, 0) // + .input("evcs0", IS_CLUSTERED, false) // + .input(GRID_ACTIVE_POWER, -6_000) // + .input("evcs0", ACTIVE_POWER, 0) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 6_000)) // + .next(new TestCase() // + .input(ESS_DISCHARGE_POWER, 0) // + .input(GRID_ACTIVE_POWER, -200) // + .input("evcs0", ACTIVE_POWER, 5800) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 6_000)) // + .next(new TestCase() // + .input(ESS_DISCHARGE_POWER, 0) // + .input(GRID_ACTIVE_POWER, 500) // + .input("evcs0", ACTIVE_POWER, 5800) // + .input("evcs0", Evcs.ChannelId.MINIMUM_HARDWARE_POWER, Evcs.DEFAULT_MINIMUM_HARDWARE_POWER) + .output("evcs0", SET_CHARGE_POWER_LIMIT, 5300)) + + // Active hysteresis + .next(new TestCase() // + .input(ESS_DISCHARGE_POWER, 0) // + .input(GRID_ACTIVE_POWER, 1000) // + .input("evcs0", ACTIVE_POWER, 5000) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 5_300) // + .output(AWAITING_HYSTERESIS, true)) // + .next(new TestCase() // + .timeleap(clock, 6, MINUTES)) // + + // Passed hysteresis + .next(new TestCase() // + .input(ESS_DISCHARGE_POWER, 0) // + .input(GRID_ACTIVE_POWER, 1000) // + .input("evcs0", ACTIVE_POWER, 5000) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 0) // + .output(AWAITING_HYSTERESIS, false)) // + + // Active hysteresis + .next(new TestCase() // + .input(ESS_DISCHARGE_POWER, 0) // + .input(GRID_ACTIVE_POWER, -5000) // + .input("evcs0", ACTIVE_POWER, 0) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 0) // + .output(AWAITING_HYSTERESIS, true)) + + .next(new TestCase() // + .timeleap(clock, 1, MINUTES)) // + + // New charge process starting after another 30 seconds .next(new TestCase() // - .input(sumEssDischargePower, -6000) // - .input(evcs0IsClustered, true) // - .input(sumGridActivePower, 0) // - .input(evcs0ChargePower, 0) // - .input(evcs0Status, Status.CHARGING_REJECTED) // - .output(evcs0SetChargePowerLimit, 0) // - .output(evcs0MaximumPower, null)) // - ; + .input(ESS_DISCHARGE_POWER, 0) // + .input(GRID_ACTIVE_POWER, -5000) // + .input("evcs0", ACTIVE_POWER, 0) // + .output("evcs0", SET_CHARGE_POWER_LIMIT, 5000) // + .output(AWAITING_HYSTERESIS, false)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/MyConfig.java b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/MyConfig.java index 5e8533f1b41..cf509aca506 100644 --- a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/MyConfig.java +++ b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/MyConfig.java @@ -17,6 +17,8 @@ protected static class Builder { private int defaultChargeMinPower = 0; private Priority priority = Priority.CAR; private int energySessionLimit = 0; + private int excessChargeHystersis = 120; + private int excessChargePauseHysteresis = 30; private Builder() { } @@ -71,6 +73,16 @@ public Builder setEnergySessionLimit(int energySessionLimit) { return this; } + public Builder setExcessChargeHystersis(int excessChargeHystersis) { + this.excessChargeHystersis = excessChargeHystersis; + return this; + } + + public Builder setExcessChargePauseHysteresis(int excessChargePauseHysteresis) { + this.excessChargePauseHysteresis = excessChargePauseHysteresis; + return this; + } + public MyConfig build() { return new MyConfig(this); } @@ -137,6 +149,16 @@ public boolean debugMode() { return this.builder.debugMode; } + @Override + public int excessChargeHystersis() { + return this.builder.excessChargeHystersis; + } + + @Override + public int excessChargePauseHysteresis() { + return this.builder.excessChargePauseHysteresis; + } + @Override public String evcs_target() { return "(&(enabled=true)(!(service.pid=ctrlEvcs0))(|(id=" + this.evcs_id() + ")))"; diff --git a/io.openems.edge.controller.generic.jsonlogic/test/io/openems/edge/controller/generic/jsonlogic/ControllerGenericJsonLogicImplTest.java b/io.openems.edge.controller.generic.jsonlogic/test/io/openems/edge/controller/generic/jsonlogic/ControllerGenericJsonLogicImplTest.java index 943591e76dd..f0153a6af4e 100644 --- a/io.openems.edge.controller.generic.jsonlogic/test/io/openems/edge/controller/generic/jsonlogic/ControllerGenericJsonLogicImplTest.java +++ b/io.openems.edge.controller.generic.jsonlogic/test/io/openems/edge/controller/generic/jsonlogic/ControllerGenericJsonLogicImplTest.java @@ -1,10 +1,11 @@ package io.openems.edge.controller.generic.jsonlogic; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.SOC; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; -import io.openems.edge.common.sum.Sum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; @@ -12,40 +13,32 @@ public class ControllerGenericJsonLogicImplTest { - private static final ChannelAddress ESS_SOC = new ChannelAddress(Sum.SINGLETON_COMPONENT_ID, - Sum.ChannelId.ESS_SOC.id()); - - private static final String ESS_ID = "ess0"; - - private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - @Test public void test() throws Exception { new ControllerTest(new ControllerGenericJsonLogicImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addComponent(new DummySum()) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID)) // + .addComponent(new DummyManagedSymmetricEss("ess0")) // .activate(MyConfig.create() // .setRule("{" // + " \"if\":["// + " {"// + " \"<\": ["// + " {"// - + " \"var\": \"" + ESS_SOC + "\""// + + " \"var\": \"ess0/Soc\""// + " },"// + " 50"// + " ]"// + " },"// + " ["// + " ["// - + " \"" + ESS_SET_ACTIVE_POWER_EQUALS + "\","// + + " \"ess0/SetActivePowerEquals\","// + " 5000"// + " ]"// + " ],"// + " ["// + " ["// - + " \"" + ESS_SET_ACTIVE_POWER_EQUALS + "\","// + + " \"ess0/SetActivePowerEquals\","// + " -2000"// + " ]"// + " ]"// @@ -53,12 +46,12 @@ public void test() throws Exception { + "}") // .build()) .next(new TestCase() // - .input(ESS_SOC, 40) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 5000)) // + .input("ess0", SOC, 40) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 5000)) // .next(new TestCase() // - .input(ESS_SOC, 60) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, -2000) // - ); + .input("ess0", SOC, 60) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, -2000)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.generic.jsonlogic/test/io/openems/edge/controller/generic/jsonlogic/ControllerGenericJsonLogicImplTest2.java b/io.openems.edge.controller.generic.jsonlogic/test/io/openems/edge/controller/generic/jsonlogic/ControllerGenericJsonLogicImplTest2.java index d12b3dc936c..61d21f86d56 100644 --- a/io.openems.edge.controller.generic.jsonlogic/test/io/openems/edge/controller/generic/jsonlogic/ControllerGenericJsonLogicImplTest2.java +++ b/io.openems.edge.controller.generic.jsonlogic/test/io/openems/edge/controller/generic/jsonlogic/ControllerGenericJsonLogicImplTest2.java @@ -1,10 +1,14 @@ package io.openems.edge.controller.generic.jsonlogic; +import static io.openems.edge.common.sum.Sum.ChannelId.ESS_SOC; +import static io.openems.edge.common.sum.Sum.ChannelId.PRODUCTION_ACTIVE_POWER; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT1; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT2; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; -import io.openems.edge.common.sum.Sum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; @@ -12,29 +16,19 @@ public class ControllerGenericJsonLogicImplTest2 { - private static final ChannelAddress SUM_PRODUCTION_POWER = new ChannelAddress(Sum.SINGLETON_COMPONENT_ID, - Sum.ChannelId.PRODUCTION_ACTIVE_POWER.id()); - private static final ChannelAddress SUM_SOC = new ChannelAddress(Sum.SINGLETON_COMPONENT_ID, - Sum.ChannelId.ESS_SOC.id()); - - private static final String IO_ID = "io0"; - private static final ChannelAddress INPUT0 = new ChannelAddress(IO_ID, "InputOutput0"); - private static final ChannelAddress OUTPUT0 = new ChannelAddress(IO_ID, "InputOutput1"); - private static final ChannelAddress OUTPUT1 = new ChannelAddress(IO_ID, "InputOutput2"); - @Test public void test() throws Exception { new ControllerTest(new ControllerGenericJsonLogicImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addComponent(new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // .setRule("{"// + " \"if\": ["// + " {"// + " \">\": ["// + " {"// - + " \"var\": \"" + SUM_PRODUCTION_POWER + "\""// + + " \"var\": \"_sum/ProductionActivePower\""// + " },"// + " 2000"// + " ]"// @@ -44,7 +38,7 @@ public void test() throws Exception { + " {"// + " \">\": ["// + " {"// - + " \"var\": \"" + SUM_SOC + "\""// + + " \"var\": \"_sum/EssSoc\""// + " },"// + " 70"// + " ]"// @@ -52,24 +46,24 @@ public void test() throws Exception { + " {"// + " \"if\": ["// + " {"// - + " \"var\": \"" + INPUT0 + "\""// + + " \"var\": \"io0/InputOutput0\""// + " },"// + " ["// + " ],"// + " {"// + " \"if\": ["// + " {"// - + " \"var\": \"" + OUTPUT0 + "\""// + + " \"var\": \"io0/InputOutput1\""// + " },"// + " ["// + " ["// - + " \"" + OUTPUT0 + "\","// + + " \"io0/InputOutput1\","// + " false"// + " ]"// + " ],"// + " ["// + " ["// - + " \"" + OUTPUT1 + "\","// + + " \"io0/InputOutput2\","// + " true"// + " ]"// + " ]"// @@ -86,7 +80,7 @@ public void test() throws Exception { + " {"// + " \"<\": ["// + " {"// - + " \"var\": \"" + SUM_SOC + "\""// + + " \"var\": \"_sum/EssSoc\""// + " },"// + " 40"// + " ]"// @@ -94,24 +88,24 @@ public void test() throws Exception { + " {"// + " \"if\": ["// + " {"// - + " \"var\": \"" + INPUT0 + "\""// + + " \"var\": \"io0/InputOutput0\""// + " },"// + " ["// + " ],"// + " {"// + " \"if\": ["// + " {"// - + " \"var\": \"" + OUTPUT1 + "\""// + + " \"var\": \"io0/InputOutput2\""// + " },"// + " ["// + " ["// - + " \"" + OUTPUT1 + "\","// + + " \"io0/InputOutput2\","// + " false"// + " ]"// + " ],"// + " ["// + " ["// - + " \"" + OUTPUT0 + "\","// + + " \"io0/InputOutput1\","// + " true"// + " ]"// + " ]"// @@ -127,30 +121,30 @@ public void test() throws Exception { + "}") // .build()) .next(new TestCase() // - .input(SUM_PRODUCTION_POWER, 2001) // - .input(SUM_SOC, 71) // - .input(INPUT0, false) // - .input(OUTPUT0, true) // - .output(OUTPUT0, false)) // + .input(PRODUCTION_ACTIVE_POWER, 2001) // + .input(ESS_SOC, 71) // + .input("io0", INPUT_OUTPUT0, false) // + .input("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT1, false)) // .next(new TestCase() // - .input(SUM_PRODUCTION_POWER, 2001) // - .input(SUM_SOC, 71) // - .input(INPUT0, false) // - .input(OUTPUT0, false) // - .output(OUTPUT1, true)) // + .input(PRODUCTION_ACTIVE_POWER, 2001) // + .input(ESS_SOC, 71) // + .input("io0", INPUT_OUTPUT0, false) // + .input("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, true)) // .next(new TestCase() // - .input(SUM_PRODUCTION_POWER, 1999) // - .input(SUM_SOC, 39) // - .input(INPUT0, false) // - .input(OUTPUT1, true) // - .output(OUTPUT1, false)) // + .input(PRODUCTION_ACTIVE_POWER, 1999) // + .input(ESS_SOC, 39) // + .input("io0", INPUT_OUTPUT0, false) // + .input("io0", INPUT_OUTPUT2, true) // + .output("io0", INPUT_OUTPUT2, false)) // .next(new TestCase() // - .input(SUM_PRODUCTION_POWER, 1999) // - .input(SUM_SOC, 39) // - .input(INPUT0, false) // - .input(OUTPUT1, false) // - .output(OUTPUT0, true)) // - ; + .input(PRODUCTION_ACTIVE_POWER, 1999) // + .input(ESS_SOC, 39) // + .input("io0", INPUT_OUTPUT0, false) // + .input("io0", INPUT_OUTPUT2, false) // + .output("io0", INPUT_OUTPUT1, true)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.highloadtimeslot/test/io/openems/edge/controller/highloadtimeslot/ControllerHighLoadTimeslotImplTest.java b/io.openems.edge.controller.highloadtimeslot/test/io/openems/edge/controller/highloadtimeslot/ControllerHighLoadTimeslotImplTest.java index 042f700767e..ad6992ddea2 100644 --- a/io.openems.edge.controller.highloadtimeslot/test/io/openems/edge/controller/highloadtimeslot/ControllerHighLoadTimeslotImplTest.java +++ b/io.openems.edge.controller.highloadtimeslot/test/io/openems/edge/controller/highloadtimeslot/ControllerHighLoadTimeslotImplTest.java @@ -1,5 +1,7 @@ package io.openems.edge.controller.highloadtimeslot; +import static io.openems.edge.controller.highloadtimeslot.WeekdayFilter.EVERDAY; + import org.junit.Test; import io.openems.edge.common.test.DummyComponentManager; @@ -7,24 +9,22 @@ public class ControllerHighLoadTimeslotImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerHighLoadTimeslotImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEss(ESS_ID).setHysteresisSoc(90) // + .setId("ctrl0") // + .setEss("ess0") // + .setHysteresisSoc(90) // .setChargePower(10000) // .setDischargePower(20000) // .setStartDate("01.01.2019") // .setEndDate("01.01.2020") // .setStartTime("08:00") // .setEndTime("13:00") // - .setWeekdayFilter(WeekdayFilter.EVERDAY) // - .build()); // - ; + .setWeekdayFilter(EVERDAY) // + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.io.alarm/test/io/openems/edge/controller/io/alarm/ControllerIoAlarmImplTest.java b/io.openems.edge.controller.io.alarm/test/io/openems/edge/controller/io/alarm/ControllerIoAlarmImplTest.java index d4adfec3d38..4eb47a82b4b 100644 --- a/io.openems.edge.controller.io.alarm/test/io/openems/edge/controller/io/alarm/ControllerIoAlarmImplTest.java +++ b/io.openems.edge.controller.io.alarm/test/io/openems/edge/controller/io/alarm/ControllerIoAlarmImplTest.java @@ -1,8 +1,11 @@ package io.openems.edge.controller.io.alarm; +import static io.openems.edge.controller.io.alarm.DummyComponent.ChannelId.STATE_0; +import static io.openems.edge.controller.io.alarm.DummyComponent.ChannelId.STATE_1; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; @@ -10,44 +13,34 @@ public class ControllerIoAlarmImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String DUMMY_ID = "dummy0"; - private static final ChannelAddress DUMMY_STATE0 = new ChannelAddress(DUMMY_ID, "State0"); - private static final ChannelAddress DUMMY_STATE1 = new ChannelAddress(DUMMY_ID, "State1"); - - private static final String IO_ID = "io0"; - private static final ChannelAddress IO_INPUT_OUTPUT0 = new ChannelAddress(IO_ID, "InputOutput0"); - @Test public void test() throws Exception { new ControllerTest(new ControllerIoAlarmImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyComponent(DUMMY_ID)) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyComponent("dummy0")) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setInputChannelAddresses(// - DUMMY_STATE0.toString(), // - DUMMY_STATE1.toString()) - .setOutputChannelAddress(IO_INPUT_OUTPUT0.toString()) // + .setId("ctrl0") // + .setInputChannelAddresses("dummy0/State0", "dummy0/State1") + .setOutputChannelAddress("io0/InputOutput0") // .build()) .next(new TestCase() // - .input(DUMMY_STATE0, true) // - .input(DUMMY_STATE1, false) // - .output(IO_INPUT_OUTPUT0, true)) // + .input("dummy0", STATE_0, true) // + .input("dummy0", STATE_1, false) // + .output("io0", INPUT_OUTPUT0, true)) // .next(new TestCase() // - .input(DUMMY_STATE0, false) // - .input(DUMMY_STATE1, true) // - .output(IO_INPUT_OUTPUT0, true)) // + .input("dummy0", STATE_0, false) // + .input("dummy0", STATE_1, true) // + .output("io0", INPUT_OUTPUT0, true)) // .next(new TestCase() // - .input(DUMMY_STATE0, true) // - .input(DUMMY_STATE1, true) // - .output(IO_INPUT_OUTPUT0, true)) + .input("dummy0", STATE_0, true) // + .input("dummy0", STATE_1, true) // + .output("io0", INPUT_OUTPUT0, true)) .next(new TestCase() // - .input(DUMMY_STATE0, false) // - .input(DUMMY_STATE1, false) // - .output(IO_INPUT_OUTPUT0, false)); + .input("dummy0", STATE_0, false) // + .input("dummy0", STATE_1, false) // + .output("io0", INPUT_OUTPUT0, false)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java index 6c31ef34601..e2ce7b1917c 100644 --- a/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java +++ b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java @@ -1,14 +1,14 @@ package io.openems.edge.controller.io.analog; -import java.time.Instant; -import java.time.ZoneOffset; +import static io.openems.edge.common.test.TestUtils.createDummyClock; + import java.time.temporal.ChronoUnit; import org.junit.Test; -import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; +import io.openems.edge.common.sum.Sum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.controller.test.ControllerTest; @@ -18,139 +18,129 @@ public class MyControllerTest { - private static final String CTRL_ID = "ctrl0"; - private static final String IO_ID = "analogIo0"; - - // Sum channels - private static final ChannelAddress SUM_ESS_DISCHARGE_POWER = new ChannelAddress("_sum", "EssDischargePower"); - private static final ChannelAddress SUM_GRID_ACTIVE_POWER = new ChannelAddress("_sum", "GridActivePower"); - - // AnalogIO channels - private static final ChannelAddress DEBUG_SET_OUTPUT_VOLTAGE = new ChannelAddress(IO_ID, "DebugSetOutputVoltage"); - private static final ChannelAddress DEBUG_SET_OUTPUT_PERCENT = new ChannelAddress(IO_ID, "DebugSetOutputPercent"); + private static final ChannelAddress DEBUG_SET_OUTPUT_VOLTAGE = new ChannelAddress("analogIo0", + "DebugSetOutputVoltage"); + private static final ChannelAddress DEBUG_SET_OUTPUT_PERCENT = new ChannelAddress("analogIo0", + "DebugSetOutputPercent"); @Test public void testOff() throws Exception { - - final var analogOutput = new DummyAnalogVoltageOutput(IO_ID); - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); new ControllerTest(new ControllerIoAnalogImpl(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // - .addReference("analogOutput", analogOutput) // + .addReference("analogOutput", new DummyAnalogVoltageOutput("analogIo0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setAnalogOutputId(IO_ID) // + .setId("ctrl0") // + .setAnalogOutputId("analogIo0") // .setManualTarget(6_000) // .setMaximumPower(10_000) // .setMode(Mode.OFF) // .setPowerBehaviour(PowerBehavior.LINEAR) // .build()) .next(new TestCase() // - .input(SUM_ESS_DISCHARGE_POWER, 2000) // - .input(SUM_GRID_ACTIVE_POWER, -5000)// + .input(Sum.ChannelId.ESS_DISCHARGE_POWER, 2000) // + .input(Sum.ChannelId.GRID_ACTIVE_POWER, -5000)// .output(DEBUG_SET_OUTPUT_PERCENT, 0f) // .output(DEBUG_SET_OUTPUT_VOLTAGE, 0)) // - ; + .deactivate(); } @Test public void testOn() throws Exception { - - final var analogOutput = new DummyAnalogVoltageOutput(IO_ID) // - .setRange(new Range(0, 100, 10000)); - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); new ControllerTest(new ControllerIoAnalogImpl(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // - .addReference("analogOutput", analogOutput) // + .addReference("analogOutput", new DummyAnalogVoltageOutput("analogIo0") // + .setRange(new Range(0, 100, 10000))) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setAnalogOutputId(IO_ID) // + .setId("ctrl0") // + .setAnalogOutputId("analogIo0") // .setManualTarget(6_000) // .setMaximumPower(10_000) // .setMode(Mode.ON) // .setPowerBehaviour(PowerBehavior.LINEAR) // .build()) .next(new TestCase() // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, -5000)// + .input(Sum.ChannelId.ESS_DISCHARGE_POWER, 0) // + .input(Sum.ChannelId.GRID_ACTIVE_POWER, -5000)// .output(DEBUG_SET_OUTPUT_PERCENT, 60.000004f) // .output(DEBUG_SET_OUTPUT_VOLTAGE, 6000)) // - ; + .deactivate(); new ControllerTest(new ControllerIoAnalogImpl(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // - .addReference("analogOutput", analogOutput) // + .addReference("analogOutput", new DummyAnalogVoltageOutput("analogIo0") // + .setRange(new Range(0, 100, 10000))) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setAnalogOutputId(IO_ID) // + .setId("ctrl0") // + .setAnalogOutputId("analogIo0") // .setManualTarget(0) // .setMaximumPower(10_000) // .setMode(Mode.ON) // .setPowerBehaviour(PowerBehavior.LINEAR) // .build()) .next(new TestCase() // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, -5000)// + .input(Sum.ChannelId.ESS_DISCHARGE_POWER, 0) // + .input(Sum.ChannelId.GRID_ACTIVE_POWER, -5000)// .output(DEBUG_SET_OUTPUT_PERCENT, 0f) // .output(DEBUG_SET_OUTPUT_VOLTAGE, 0)) // - ; + .deactivate(); } @Test public void testAutomatic() throws Exception { - - final var analogOutput = new DummyAnalogVoltageOutput(IO_ID); - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); new ControllerTest(new ControllerIoAnalogImpl(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // .addReference("timedata", new DummyTimedata("timedata0")) // - .addReference("analogOutput", analogOutput) // + .addReference("analogOutput", new DummyAnalogVoltageOutput("analogIo0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setAnalogOutputId(IO_ID) // + .setId("ctrl0") // + .setAnalogOutputId("analogIo0") // .setManualTarget(6_000) // .setMaximumPower(10_000) // .setMode(Mode.AUTOMATIC) // .setPowerBehaviour(PowerBehavior.LINEAR) // .build()) .next(new TestCase() // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, -5000)// + .input(Sum.ChannelId.ESS_DISCHARGE_POWER, 0) // + .input(Sum.ChannelId.GRID_ACTIVE_POWER, -5000)// .output(DEBUG_SET_OUTPUT_PERCENT, 50f) // .output(DEBUG_SET_OUTPUT_VOLTAGE, 5000)) // .next(new TestCase() // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, -2444)// + .input(Sum.ChannelId.ESS_DISCHARGE_POWER, 0) // + .input(Sum.ChannelId.GRID_ACTIVE_POWER, -2444)// .output(DEBUG_SET_OUTPUT_PERCENT, 50f) // .output(DEBUG_SET_OUTPUT_VOLTAGE, 5000)) // .next(new TestCase() // .timeleap(clock, 4, ChronoUnit.SECONDS) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, -2444)// + .input(Sum.ChannelId.ESS_DISCHARGE_POWER, 0) // + .input(Sum.ChannelId.GRID_ACTIVE_POWER, -2444)// .output(DEBUG_SET_OUTPUT_PERCENT, 24.439999f) // .output(DEBUG_SET_OUTPUT_VOLTAGE, 2400)) // 100mV Steps .next(new TestCase() // .timeleap(clock, 4, ChronoUnit.SECONDS) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, -12444)// + .input(Sum.ChannelId.ESS_DISCHARGE_POWER, 0) // + .input(Sum.ChannelId.GRID_ACTIVE_POWER, -12444)// .output(DEBUG_SET_OUTPUT_PERCENT, 100f) // .output(DEBUG_SET_OUTPUT_VOLTAGE, 10000)) // .next(new TestCase() // .timeleap(clock, 4, ChronoUnit.SECONDS) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_GRID_ACTIVE_POWER, 1000)// + .input(Sum.ChannelId.ESS_DISCHARGE_POWER, 0) // + .input(Sum.ChannelId.GRID_ACTIVE_POWER, 1000)// .output(DEBUG_SET_OUTPUT_PERCENT, 0f) // .output(DEBUG_SET_OUTPUT_VOLTAGE, 0)) // - ; + + .deactivate(); } } diff --git a/io.openems.edge.controller.io.channelsinglethreshold/test/io/openems/edge/controller/io/channelsinglethreshold/ControllerIoChannelSingleThresholdImplTest.java b/io.openems.edge.controller.io.channelsinglethreshold/test/io/openems/edge/controller/io/channelsinglethreshold/ControllerIoChannelSingleThresholdImplTest.java index 7a780ffba36..19494c4a3f3 100644 --- a/io.openems.edge.controller.io.channelsinglethreshold/test/io/openems/edge/controller/io/channelsinglethreshold/ControllerIoChannelSingleThresholdImplTest.java +++ b/io.openems.edge.controller.io.channelsinglethreshold/test/io/openems/edge/controller/io/channelsinglethreshold/ControllerIoChannelSingleThresholdImplTest.java @@ -1,9 +1,11 @@ package io.openems.edge.controller.io.channelsinglethreshold; +import static io.openems.edge.controller.io.channelsinglethreshold.ControllerIoChannelSingleThreshold.ChannelId.AWAITING_HYSTERESIS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.SOC; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; + import org.junit.Test; -import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; @@ -12,35 +14,26 @@ public class ControllerIoChannelSingleThresholdImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final ChannelAddress CTRL_AWAITING_HYSTERESIS = new ChannelAddress(CTRL_ID, "AwaitingHysteresis"); - - private static final String ESS_ID = "ess0"; - private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); - - private static final String IO_ID = "io0"; - private static final ChannelAddress IO_INPUT_OUTPUT0 = new ChannelAddress(IO_ID, "InputOutput0"); - @Test public void test() throws Exception { - final var clock = new TimeLeapClock(); new ControllerTest(new ControllerIoChannelSingleThresholdImpl()) // - .addReference("componentManager", new DummyComponentManager(clock)) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID)) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addReference("componentManager", new DummyComponentManager()) // + .addComponent(new DummyManagedSymmetricEss("ess0")) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setMode(Mode.AUTOMATIC) // - .setInputChannelAddress(ESS_SOC.toString()) // - .setOutputChannelAddress(IO_INPUT_OUTPUT0.toString()) // + .setInputChannelAddress("ess0/Soc") // + .setOutputChannelAddress("io0/InputOutput0") // .setThreshold(70) // .setSwitchedLoadPower(0) // .setMinimumSwitchingTime(60).setInvert(false) // .build()) .next(new TestCase() // - .input(ESS_SOC, 50) // - .output(IO_INPUT_OUTPUT0, false) // - .output(CTRL_AWAITING_HYSTERESIS, false)); // + .input("ess0", SOC, 50) // + .output("io0", INPUT_OUTPUT0, false) // + .output(AWAITING_HYSTERESIS, false)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.io.fixdigitaloutput/test/io/openems/edge/controller/io/fixdigitaloutput/ControllerIoFixDigitalOutputImplTest.java b/io.openems.edge.controller.io.fixdigitaloutput/test/io/openems/edge/controller/io/fixdigitaloutput/ControllerIoFixDigitalOutputImplTest.java index 9a80d917290..87dc6f60fb3 100644 --- a/io.openems.edge.controller.io.fixdigitaloutput/test/io/openems/edge/controller/io/fixdigitaloutput/ControllerIoFixDigitalOutputImplTest.java +++ b/io.openems.edge.controller.io.fixdigitaloutput/test/io/openems/edge/controller/io/fixdigitaloutput/ControllerIoFixDigitalOutputImplTest.java @@ -1,8 +1,9 @@ package io.openems.edge.controller.io.fixdigitaloutput; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; @@ -10,11 +11,6 @@ public class ControllerIoFixDigitalOutputImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String IO_ID = "io0"; - private static final ChannelAddress IO_INPUT_OUTPUT0 = new ChannelAddress(IO_ID, "InputOutput0"); - @Test public void testOn() throws Exception { this.testSwitch(true); @@ -28,14 +24,14 @@ public void testOff() throws Exception { private void testSwitch(boolean on) throws Exception { new ControllerTest(new ControllerIoFixDigitalOutputImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setOutputChannelAddress(IO_INPUT_OUTPUT0.toString()) // + .setId("ctrl0") // + .setOutputChannelAddress("io0/InputOutput0") // .setOn(on) // .build()) .next(new TestCase() // - .output(IO_INPUT_OUTPUT0, on)); + .output("io0", INPUT_OUTPUT0, on)) // + .deactivate(); } - } diff --git a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerHeatingElementImplTest4.java b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerHeatingElementImplTest4.java index 854d4f90087..6e5db300d23 100644 --- a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerHeatingElementImplTest4.java +++ b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerHeatingElementImplTest4.java @@ -1,18 +1,18 @@ package io.openems.edge.controller.io.heatingelement; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; +import static io.openems.edge.common.sum.Sum.ChannelId.ESS_DISCHARGE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_ACTIVE_POWER; +import static io.openems.edge.controller.io.heatingelement.ControllerIoHeatingElement.ChannelId.LEVEL; +import static java.time.temporal.ChronoUnit.SECONDS; import org.junit.Test; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; -import io.openems.edge.common.sum.Sum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.TestUtils; import io.openems.edge.controller.io.heatingelement.enums.Level; import io.openems.edge.controller.io.heatingelement.enums.Mode; import io.openems.edge.controller.io.heatingelement.enums.WorkMode; @@ -20,25 +20,20 @@ import io.openems.edge.io.test.DummyInputOutput; public class ControllerHeatingElementImplTest4 { - private static final String CTRL_ID = "ctrl0"; - private static final String IO_ID = "io0"; - private static final ChannelAddress IO_OUTPUT1 = new ChannelAddress(IO_ID, "InputOutput1"); - private static final ChannelAddress IO_OUTPUT2 = new ChannelAddress(IO_ID, "InputOutput2"); - private static final ChannelAddress IO_OUTPUT3 = new ChannelAddress(IO_ID, "InputOutput3"); - private static final TimeLeapClock clock = new TimeLeapClock( - Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, ZoneOffset.UTC); + private static TimeLeapClock clock; private static ControllerTest prepareTest(Mode mode, Level level) throws OpenemsNamedException, Exception { + clock = TestUtils.createDummyClock(); return new ControllerTest(new ControllerIoHeatingElementImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setOutputChannelPhaseL1(IO_OUTPUT1.toString()) // - .setOutputChannelPhaseL2(IO_OUTPUT2.toString()) // - .setOutputChannelPhaseL3(IO_OUTPUT3.toString()) // + .setId("ctrl0") // + .setOutputChannelPhaseL1("io0/InputOutput0") // + .setOutputChannelPhaseL2("io0/InputOutput1") // + .setOutputChannelPhaseL3("io0/InputOutput2") // .setEndTime("15:45:00") // .setPowerOfPhase(2000) // .setMode(mode) // @@ -49,13 +44,6 @@ private static ControllerTest prepareTest(Mode mode, Level level) throws Openems .build()); // } - private static final ChannelAddress ESSO_DISCHARGE_POWER = new ChannelAddress("_sum", - Sum.ChannelId.ESS_DISCHARGE_POWER.id()); - private static final ChannelAddress LEVEL = new ChannelAddress(CTRL_ID, - ControllerIoHeatingElement.ChannelId.LEVEL.id()); - private static final ChannelAddress GRID_ACTIVE_POWER = new ChannelAddress("_sum", - Sum.ChannelId.GRID_ACTIVE_POWER.id()); - @Test public void testDischargeTakeIntoAccount() throws OpenemsNamedException, Exception { prepareTest(Mode.AUTOMATIC, Level.LEVEL_3)// @@ -63,20 +51,20 @@ public void testDischargeTakeIntoAccount() throws OpenemsNamedException, Excepti .input(GRID_ACTIVE_POWER, -2500)// .output(LEVEL, Level.LEVEL_1)) // .next(new TestCase() // - .timeleap(clock, 181, ChronoUnit.SECONDS)// + .timeleap(clock, 181, SECONDS)// .input(GRID_ACTIVE_POWER, -2500)// .output(LEVEL, Level.LEVEL_2))// // Grid power reducing because of 2kW heating power .next(new TestCase()// - .timeleap(clock, 181, ChronoUnit.SECONDS)// + .timeleap(clock, 181, SECONDS)// .input(GRID_ACTIVE_POWER, -500) // - .output(LEVEL, Level.LEVEL_2)// - ).next(new TestCase() // - .timeleap(clock, 181, ChronoUnit.SECONDS)// + .output(LEVEL, Level.LEVEL_2)) // + .next(new TestCase() // + .timeleap(clock, 181, SECONDS)// .input(GRID_ACTIVE_POWER, -500) // - .input(ESSO_DISCHARGE_POWER, 2300) // - .output(LEVEL, Level.LEVEL_1)// - ); // ; + .input(ESS_DISCHARGE_POWER, 2300) // + .output(LEVEL, Level.LEVEL_1)) // + .deactivate(); } @Test @@ -87,9 +75,10 @@ public void realDataTest() throws OpenemsNamedException, Exception { .input(GRID_ACTIVE_POWER, -6000)// .output(LEVEL, Level.LEVEL_3)) // .next(new TestCase()// - .timeleap(clock, 181, ChronoUnit.SECONDS)// + .timeleap(clock, 181, SECONDS)// .input(GRID_ACTIVE_POWER, 0)// - .input(ESSO_DISCHARGE_POWER, 2280)// - .output(LEVEL, Level.LEVEL_1)); // ; + .input(ESS_DISCHARGE_POWER, 2280)// + .output(LEVEL, Level.LEVEL_1)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest.java b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest.java index a2c130c4c3a..f874c0994e7 100644 --- a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest.java +++ b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest.java @@ -1,17 +1,22 @@ package io.openems.edge.controller.io.heatingelement; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_ACTIVE_POWER; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.controller.io.heatingelement.ControllerIoHeatingElement.ChannelId.PHASE1_TIME; +import static io.openems.edge.controller.io.heatingelement.ControllerIoHeatingElement.ChannelId.PHASE2_TIME; +import static io.openems.edge.controller.io.heatingelement.ControllerIoHeatingElement.ChannelId.PHASE3_TIME; +import static io.openems.edge.controller.io.heatingelement.enums.Level.LEVEL_3; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT1; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT2; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MINUTES; import org.junit.Test; -import io.openems.common.test.TimeLeapClock; -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.DummyComponentManager; -import io.openems.edge.controller.io.heatingelement.enums.Level; import io.openems.edge.controller.io.heatingelement.enums.Mode; import io.openems.edge.controller.io.heatingelement.enums.WorkMode; import io.openems.edge.controller.test.ControllerTest; @@ -19,36 +24,22 @@ public class ControllerIoHeatingElementImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String IO_ID = "io0"; - - private static final ChannelAddress SUM_GRID_ACTIVE_POWER = new ChannelAddress("_sum", "GridActivePower"); - - private static final ChannelAddress IO_OUTPUT1 = new ChannelAddress(IO_ID, "InputOutput1"); - private static final ChannelAddress IO_OUTPUT2 = new ChannelAddress(IO_ID, "InputOutput2"); - private static final ChannelAddress IO_OUTPUT3 = new ChannelAddress(IO_ID, "InputOutput3"); - - private static final ChannelAddress CTRL_PHASE1TIME = new ChannelAddress(CTRL_ID, "Phase1Time"); - private static final ChannelAddress CTRL_PHASE2TIME = new ChannelAddress(CTRL_ID, "Phase2Time"); - private static final ChannelAddress CTRL_PHASE3TIME = new ChannelAddress(CTRL_ID, "Phase3Time"); - @Test public void test() throws Exception { - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); + final var clock = createDummyClock(); new ControllerTest(new ControllerIoHeatingElementImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setOutputChannelPhaseL1(IO_OUTPUT1.toString()) // - .setOutputChannelPhaseL2(IO_OUTPUT2.toString()) // - .setOutputChannelPhaseL3(IO_OUTPUT3.toString()) // + .setId("ctrl0") // + .setOutputChannelPhaseL1("io0/InputOutput0") // + .setOutputChannelPhaseL2("io0/InputOutput1") // + .setOutputChannelPhaseL3("io0/InputOutput2") // .setEndTime("15:45:00") // .setPowerOfPhase(2000) // .setMode(Mode.AUTOMATIC) // - .setDefaultLevel(Level.LEVEL_3) // + .setDefaultLevel(LEVEL_3) // .setWorkMode(WorkMode.TIME) // .setMinTime(1) // .setMinimumSwitchingTime(60) // @@ -56,133 +47,134 @@ public void test() throws Exception { .next(new TestCase() // // Grid active power : 0, Excess power : 0, // from -> UNDEFINED --to--> LEVEL_0, no of relais = 0 - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, false) // - .output(IO_OUTPUT2, false) // - .output(IO_OUTPUT3, false) // - .output(CTRL_PHASE1TIME, 0) // - .output(CTRL_PHASE1TIME, 0) // - .output(CTRL_PHASE2TIME, 0)) // + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, false) // + .output(PHASE1_TIME, 0) // + .output(PHASE1_TIME, 0) // + .output(PHASE2_TIME, 0)) // .next(new TestCase() // // Grid active power : 0, Excess power : 0, // from -> LEVEL_0 --to--> LEVEL_0, no of relais = 0 - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, false) // - .output(IO_OUTPUT2, false) // - .output(IO_OUTPUT3, false) // - .output(CTRL_PHASE1TIME, 0) // - .output(CTRL_PHASE2TIME, 0) // - .output(CTRL_PHASE3TIME, 0)) // + .timeleap(clock, 15, MINUTES)// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, false) // + .output(PHASE1_TIME, 0) // + .output(PHASE2_TIME, 0) // + .output(PHASE3_TIME, 0)) // .next(new TestCase() // // Grid active power : -2000, Excess power : 2000, // from -> LEVEL_0 --to--> LEVEL_1, no of relais = 1 - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, -2000) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, false) // - .output(IO_OUTPUT3, false) // - .output(CTRL_PHASE1TIME, 0) // - .output(CTRL_PHASE2TIME, 0) // - .output(CTRL_PHASE3TIME, 0)) // + .timeleap(clock, 15, MINUTES)// + .input(GRID_ACTIVE_POWER, -2000) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, false) // + .output(PHASE1_TIME, 0) // + .output(PHASE2_TIME, 0) // + .output(PHASE3_TIME, 0)) // .next(new TestCase() // // Grid active power : -4000, Excess power : 6000, // from -> LEVEL_1 --to--> LEVEL_3, no of relais = 3 - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, -4000) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, true) // - .output(CTRL_PHASE1TIME, 15 * 60) // - .output(CTRL_PHASE2TIME, 0) // - .output(CTRL_PHASE3TIME, 0)) // + .timeleap(clock, 15, MINUTES)// + .input(GRID_ACTIVE_POWER, -4000) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, true) // + .output(PHASE1_TIME, 15 * 60) // + .output(PHASE2_TIME, 0) // + .output(PHASE3_TIME, 0)) // .next(new TestCase() // // Grid active power : -6000, Excess power : 12000, // from -> LEVEL_3 --to--> LEVEL_3, no of relais = 3 - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, -6000) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, true) // - .output(CTRL_PHASE1TIME, 30 * 60) // - .output(CTRL_PHASE2TIME, 15 * 60) // - .output(CTRL_PHASE3TIME, 15 * 60)) // + .timeleap(clock, 15, MINUTES)// + .input(GRID_ACTIVE_POWER, -6000) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, true) // + .output(PHASE1_TIME, 30 * 60) // + .output(PHASE2_TIME, 15 * 60) // + .output(PHASE3_TIME, 15 * 60)) // .next(new TestCase() // // Grid active power : -7000, Excess power : 13000, // from -> LEVEL_3 --to--> LEVEL_3, no of relais = 3 - .input(SUM_GRID_ACTIVE_POWER, -7000) // - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, true) // - .output(CTRL_PHASE1TIME, 45 * 60) // - .output(CTRL_PHASE2TIME, 30 * 60) // - .output(CTRL_PHASE3TIME, 30 * 60)) // + .input(GRID_ACTIVE_POWER, -7000) // + .timeleap(clock, 15, MINUTES)// + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, true) // + .output(PHASE1_TIME, 45 * 60) // + .output(PHASE2_TIME, 30 * 60) // + .output(PHASE3_TIME, 30 * 60)) // .next(new TestCase() // // Grid active power : 0, Excess power : 6000, // from -> LEVEL_3 --to--> LEVEL_3, no of relais = 3 - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, true) // - .output(CTRL_PHASE1TIME, 60 * 60) // - .output(CTRL_PHASE2TIME, 45 * 60) // - .output(CTRL_PHASE3TIME, 45 * 60)) // + .timeleap(clock, 15, MINUTES)// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, true) // + .output(PHASE1_TIME, 60 * 60) // + .output(PHASE2_TIME, 45 * 60) // + .output(PHASE3_TIME, 45 * 60)) // .next(new TestCase() // // Grid active power : 1, Excess power : 0, // from -> LEVEL_3 --to--> LEVEL_0, no of relais = 0 - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, 1) // - .output(IO_OUTPUT1, false) // - .output(IO_OUTPUT2, false) // - .output(IO_OUTPUT3, false) // - .output(CTRL_PHASE1TIME, 75 * 60) // - .output(CTRL_PHASE2TIME, 60 * 60) // - .output(CTRL_PHASE3TIME, 60 * 60)) // + .timeleap(clock, 15, MINUTES)// + .input(GRID_ACTIVE_POWER, 1) // + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, false) // + .output(PHASE1_TIME, 75 * 60) // + .output(PHASE2_TIME, 60 * 60) // + .output(PHASE3_TIME, 60 * 60)) // .next(new TestCase() // // Grid active power : 20000, Excess power : 0, // from -> LEVEL_0 --to--> LEVEL_0, no of relais = 0 - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, 20000) // - .output(IO_OUTPUT1, false) // - .output(IO_OUTPUT2, false) // - .output(IO_OUTPUT3, false) // - .output(CTRL_PHASE1TIME, 75 * 60) // - .output(CTRL_PHASE2TIME, 60 * 60) // - .output(CTRL_PHASE3TIME, 60 * 60)) // + .timeleap(clock, 15, MINUTES)// + .input(GRID_ACTIVE_POWER, 20000) // + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, false) // + .output(PHASE1_TIME, 75 * 60) // + .output(PHASE2_TIME, 60 * 60) // + .output(PHASE3_TIME, 60 * 60)) // .next(new TestCase() // // Grid active power : -4000, Excess power : 10000, // from -> LEVEL_0 --to--> LEVEL_2, no of relais = 2 - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, -4000) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, false) // - .output(CTRL_PHASE1TIME, 75 * 60) // - .output(CTRL_PHASE2TIME, 60 * 60) // - .output(CTRL_PHASE3TIME, 60 * 60)) // + .timeleap(clock, 15, MINUTES)// + .input(GRID_ACTIVE_POWER, -4000) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, false) // + .output(PHASE1_TIME, 75 * 60) // + .output(PHASE2_TIME, 60 * 60) // + .output(PHASE3_TIME, 60 * 60)) // .next(new TestCase() // // Grid active power : 0, Excess power : 4000, // from -> LEVEL_2 --to--> LEVEL_2, no of relais = 2 - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, false) // - .output(CTRL_PHASE1TIME, 90 * 60) // - .output(CTRL_PHASE2TIME, 75 * 60) // - .output(CTRL_PHASE3TIME, 60 * 60)) // + .timeleap(clock, 15, MINUTES)// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, false) // + .output(PHASE1_TIME, 90 * 60) // + .output(PHASE2_TIME, 75 * 60) // + .output(PHASE3_TIME, 60 * 60)) // .next(new TestCase() // // Switch to next day - .timeleap(clock, 22, ChronoUnit.HOURS)// - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, false) // - .output(CTRL_PHASE1TIME, 30 * 60) // - .output(CTRL_PHASE2TIME, 30 * 60) // - .output(CTRL_PHASE3TIME, 0)); // + .timeleap(clock, 22, HOURS)// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, false) // + .output(PHASE1_TIME, 30 * 60) // + .output(PHASE2_TIME, 30 * 60) // + .output(PHASE3_TIME, 0)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest2.java b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest2.java index 31ec267cf14..788715d4715 100644 --- a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest2.java +++ b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest2.java @@ -1,13 +1,20 @@ package io.openems.edge.controller.io.heatingelement; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_ACTIVE_POWER; +import static io.openems.edge.controller.io.heatingelement.ControllerIoHeatingElement.ChannelId.FORCE_START_AT_SECONDS_OF_DAY; +import static io.openems.edge.controller.io.heatingelement.ControllerIoHeatingElement.ChannelId.STATUS; +import static io.openems.edge.controller.io.heatingelement.ControllerIoHeatingElement.ChannelId.TOTAL_PHASE_TIME; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT1; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT2; +import static java.time.temporal.ChronoUnit.MINUTES; + import java.time.Instant; import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; import org.junit.Test; import io.openems.common.test.TimeLeapClock; -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.DummyComponentManager; @@ -20,20 +27,6 @@ public class ControllerIoHeatingElementImplTest2 { - private static final String CTRL_ID = "ctrl0"; - private static final String IO_ID = "io0"; - - private static final ChannelAddress SUM_GRID_ACTIVE_POWER = new ChannelAddress("_sum", "GridActivePower"); - - private static final ChannelAddress IO_OUTPUT1 = new ChannelAddress(IO_ID, "InputOutput1"); - private static final ChannelAddress IO_OUTPUT2 = new ChannelAddress(IO_ID, "InputOutput2"); - private static final ChannelAddress IO_OUTPUT3 = new ChannelAddress(IO_ID, "InputOutput3"); - - private static final ChannelAddress CTRL_FORCE_START_AT_SECONDS_OF_DAY = new ChannelAddress(CTRL_ID, - "ForceStartAtSecondsOfDay"); - private static final ChannelAddress CTRL_TOTAL_PHASE_TIME = new ChannelAddress(CTRL_ID, "TotalPhaseTime"); - private static final ChannelAddress CTRL_STATUS = new ChannelAddress(CTRL_ID, "Status"); - @Test public void minimumTime_lowerLevel_test() throws Exception { final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, @@ -41,12 +34,12 @@ public void minimumTime_lowerLevel_test() throws Exception { new ControllerTest(new ControllerIoHeatingElementImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setOutputChannelPhaseL1(IO_OUTPUT1.toString()) // - .setOutputChannelPhaseL2(IO_OUTPUT2.toString()) // - .setOutputChannelPhaseL3(IO_OUTPUT3.toString()) // + .setId("ctrl0") // + .setOutputChannelPhaseL1("io0/InputOutput0") // + .setOutputChannelPhaseL2("io0/InputOutput1") // + .setOutputChannelPhaseL3("io0/InputOutput2") // .setEndTime("15:45:00") // .setPowerOfPhase(2000) // .setMode(Mode.AUTOMATIC) // @@ -56,20 +49,21 @@ public void minimumTime_lowerLevel_test() throws Exception { .setMinimumSwitchingTime(60) // .build()) // .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, false))// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, false))// .next(new TestCase() // - .timeleap(clock, 6, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, -2000) // - .output(IO_OUTPUT1, true) // - .output(CTRL_TOTAL_PHASE_TIME, 0) // - .output(CTRL_FORCE_START_AT_SECONDS_OF_DAY, 53_100 /* 14:45 */)) // + .timeleap(clock, 6, MINUTES)// + .input(GRID_ACTIVE_POWER, -2000) // + .output("io0", INPUT_OUTPUT0, true) // + .output(TOTAL_PHASE_TIME, 0) // + .output(FORCE_START_AT_SECONDS_OF_DAY, 53_100 /* 14:45 */)) // .next(new TestCase() // - .timeleap(clock, 6, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, true) // - .output(CTRL_TOTAL_PHASE_TIME, 360 /* 6 minutes, one phase */) // - .output(CTRL_FORCE_START_AT_SECONDS_OF_DAY, 53_220 /* 14:47 - two minutes later */)); // + .timeleap(clock, 6, MINUTES)// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, true) // + .output(TOTAL_PHASE_TIME, 360 /* 6 minutes, one phase */) // + .output(FORCE_START_AT_SECONDS_OF_DAY, 53_220 /* 14:47 - two minutes later */)) // + .deactivate(); } @Test @@ -79,12 +73,12 @@ public void minimumTime_sameLevel_test() throws Exception { new ControllerTest(new ControllerIoHeatingElementImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setOutputChannelPhaseL1(IO_OUTPUT1.toString()) // - .setOutputChannelPhaseL2(IO_OUTPUT2.toString()) // - .setOutputChannelPhaseL3(IO_OUTPUT3.toString()) // + .setId("ctrl0") // + .setOutputChannelPhaseL1("io0/InputOutput0") // + .setOutputChannelPhaseL2("io0/InputOutput1") // + .setOutputChannelPhaseL3("io0/InputOutput2") // .setEndTime("15:45:00") // .setPowerOfPhase(2000) // .setMode(Mode.AUTOMATIC) // @@ -94,24 +88,25 @@ public void minimumTime_sameLevel_test() throws Exception { .setMinimumSwitchingTime(60) // .build()) // .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, false))// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, false))// .next(new TestCase() // - .timeleap(clock, 6, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, -6000) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, true) // - .output(CTRL_TOTAL_PHASE_TIME, 0) // - .output(CTRL_FORCE_START_AT_SECONDS_OF_DAY, 53_100 /* 14:45 */)) // + .timeleap(clock, 6, MINUTES)// + .input(GRID_ACTIVE_POWER, -6000) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, true) // + .output(TOTAL_PHASE_TIME, 0) // + .output(FORCE_START_AT_SECONDS_OF_DAY, 53_100 /* 14:45 */)) // .next(new TestCase() // - .timeleap(clock, 6, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, true) // - .output(CTRL_TOTAL_PHASE_TIME, 1080 /* 6 minutes, all three phases */) // - .output(CTRL_FORCE_START_AT_SECONDS_OF_DAY, 53_460 /* 14:51 - six minutes later */)); // + .timeleap(clock, 6, MINUTES)// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, true) // + .output(TOTAL_PHASE_TIME, 1080 /* 6 minutes, all three phases */) // + .output(FORCE_START_AT_SECONDS_OF_DAY, 53_460 /* 14:51 - six minutes later */)) // + .deactivate(); } @Test @@ -121,12 +116,12 @@ public void minimumTime_inForceMode_test() throws Exception { new ControllerTest(new ControllerIoHeatingElementImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setOutputChannelPhaseL1(IO_OUTPUT1.toString()) // - .setOutputChannelPhaseL2(IO_OUTPUT2.toString()) // - .setOutputChannelPhaseL3(IO_OUTPUT3.toString()) // + .setId("ctrl0") // + .setOutputChannelPhaseL1("io0/InputOutput0") // + .setOutputChannelPhaseL2("io0/InputOutput1") // + .setOutputChannelPhaseL3("io0/InputOutput2") // .setEndTime("16:00:00") // .setPowerOfPhase(2000) // .setMode(Mode.AUTOMATIC) // @@ -136,35 +131,36 @@ public void minimumTime_inForceMode_test() throws Exception { .setMinimumSwitchingTime(60) // .build()) // .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, false))// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, false))// .next(new TestCase() // - .timeleap(clock, 6, ChronoUnit.MINUTES)// /* 14:57 */ - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, false) // - .output(IO_OUTPUT2, false) // - .output(IO_OUTPUT3, false) // - .output(CTRL_TOTAL_PHASE_TIME, 0) // - .output(CTRL_FORCE_START_AT_SECONDS_OF_DAY, 54_000 /* 15:00 */)) // + .timeleap(clock, 6, MINUTES)// /* 14:57 */ + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, false) // + .output(TOTAL_PHASE_TIME, 0) // + .output(FORCE_START_AT_SECONDS_OF_DAY, 54_000 /* 15:00 */)) // .next(new TestCase() // - .timeleap(clock, 4, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, 0) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, true) // - .output(CTRL_FORCE_START_AT_SECONDS_OF_DAY, 54_000 /* current time */)) + .timeleap(clock, 4, MINUTES)// + .input(GRID_ACTIVE_POWER, 0) // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, true) // + .output(FORCE_START_AT_SECONDS_OF_DAY, 54_000 /* current time */)) .next(new TestCase() // - .timeleap(clock, 3, ChronoUnit.MINUTES)// /* 15:03 */ - .input(SUM_GRID_ACTIVE_POWER, 0) // + .timeleap(clock, 3, MINUTES)// /* 15:03 */ + .input(GRID_ACTIVE_POWER, 0) // // Previous duration of each phase cannot be set as input if you want to count // already passed active time // .input(CTRL_PHASE1TIME, 180) // // .input(CTRL_PHASE2TIME, 180) // // .input(CTRL_PHASE3TIME, 180) // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, true) // - .output(CTRL_STATUS, Status.ACTIVE_FORCED) // - .output(CTRL_FORCE_START_AT_SECONDS_OF_DAY, 54_180 /* current time */)); // + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, true) // + .output(STATUS, Status.ACTIVE_FORCED) // + .output(FORCE_START_AT_SECONDS_OF_DAY, 54_180 /* current time */)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest3.java b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest3.java index f5d21a8d14d..c8d2a90b648 100644 --- a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest3.java +++ b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest3.java @@ -1,5 +1,9 @@ package io.openems.edge.controller.io.heatingelement; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT1; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT2; + import java.time.Instant; import java.time.ZoneOffset; @@ -7,7 +11,6 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.test.TimeLeapClock; -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.DummyComponentManager; @@ -19,13 +22,6 @@ public class ControllerIoHeatingElementImplTest3 { - private static final String CTRL_ID = "ctrl0"; - private static final String IO_ID = "io0"; - - private static final ChannelAddress IO_OUTPUT1 = new ChannelAddress(IO_ID, "InputOutput1"); - private static final ChannelAddress IO_OUTPUT2 = new ChannelAddress(IO_ID, "InputOutput2"); - private static final ChannelAddress IO_OUTPUT3 = new ChannelAddress(IO_ID, "InputOutput3"); - private static ControllerTest prepareTest(Mode mode, Level level) throws OpenemsNamedException, Exception { return new ControllerTest(new ControllerIoHeatingElementImpl()) // .addReference("componentManager", @@ -35,10 +31,10 @@ private static ControllerTest prepareTest(Mode mode, Level level) throws Openems .addReference("sum", new DummySum()) // .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setOutputChannelPhaseL1(IO_OUTPUT1.toString()) // - .setOutputChannelPhaseL2(IO_OUTPUT2.toString()) // - .setOutputChannelPhaseL3(IO_OUTPUT3.toString()) // + .setId("ctrl0") // + .setOutputChannelPhaseL1("io0/InputOutput0") // + .setOutputChannelPhaseL2("io0/InputOutput1") // + .setOutputChannelPhaseL3("io0/InputOutput2") // .setEndTime("15:45:00") // .setPowerOfPhase(2000) // .setMode(mode) // @@ -53,50 +49,50 @@ private static ControllerTest prepareTest(Mode mode, Level level) throws Openems public void testOff() throws Exception { prepareTest(Mode.MANUAL_OFF, Level.LEVEL_3) // .next(new TestCase() // - .output(IO_OUTPUT1, false) // - .output(IO_OUTPUT2, false) // - .output(IO_OUTPUT3, false) // - ); + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, false)) // + .deactivate(); } @Test public void testOnLevel0() throws Exception { prepareTest(Mode.MANUAL_ON, Level.LEVEL_0) // .next(new TestCase() // - .output(IO_OUTPUT1, false) // - .output(IO_OUTPUT2, false) // - .output(IO_OUTPUT3, false) // - ); + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, false)) // + .deactivate(); } @Test public void testOnLevel1() throws Exception { prepareTest(Mode.MANUAL_ON, Level.LEVEL_1) // .next(new TestCase() // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, false) // - .output(IO_OUTPUT3, false) // - ); + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, false) // + .output("io0", INPUT_OUTPUT2, false)) // + .deactivate(); } @Test public void testOnLevel2() throws Exception { prepareTest(Mode.MANUAL_ON, Level.LEVEL_2) // .next(new TestCase() // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, false) // - ); + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, false)) // + .deactivate(); } @Test public void testOnLevel3() throws Exception { prepareTest(Mode.MANUAL_ON, Level.LEVEL_3) // .next(new TestCase() // - .output(IO_OUTPUT1, true) // - .output(IO_OUTPUT2, true) // - .output(IO_OUTPUT3, true) // - ); + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true) // + .output("io0", INPUT_OUTPUT2, true)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.io.heatpump.sgready/test/io/openems/edge/controller/io/heatpump/sgready/ControllerIoHeatPumpSgReadyImplTest.java b/io.openems.edge.controller.io.heatpump.sgready/test/io/openems/edge/controller/io/heatpump/sgready/ControllerIoHeatPumpSgReadyImplTest.java index 25c1389f41a..a1e14ab2de5 100644 --- a/io.openems.edge.controller.io.heatpump.sgready/test/io/openems/edge/controller/io/heatpump/sgready/ControllerIoHeatPumpSgReadyImplTest.java +++ b/io.openems.edge.controller.io.heatpump.sgready/test/io/openems/edge/controller/io/heatpump/sgready/ControllerIoHeatPumpSgReadyImplTest.java @@ -1,13 +1,19 @@ package io.openems.edge.controller.io.heatpump.sgready; -import java.time.Instant; -import java.time.ZoneOffset; +import static io.openems.edge.common.sum.Sum.ChannelId.ESS_DISCHARGE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.ESS_SOC; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_ACTIVE_POWER; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.controller.io.heatpump.sgready.ControllerIoHeatPumpSgReady.ChannelId.AWAITING_HYSTERESIS; +import static io.openems.edge.controller.io.heatpump.sgready.ControllerIoHeatPumpSgReady.ChannelId.STATUS; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT0; +import static io.openems.edge.io.test.DummyInputOutput.ChannelId.INPUT_OUTPUT1; +import static java.time.temporal.ChronoUnit.SECONDS; + import java.time.temporal.ChronoUnit; import org.junit.Test; -import io.openems.common.test.TimeLeapClock; -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.DummyComponentManager; @@ -16,173 +22,136 @@ public class ControllerIoHeatPumpSgReadyImplTest { - private static final String CTRL_ID = "ctrHeatPump0"; - private static final String IO_ID = "io0"; - - private static final String outputChannel1 = "io0/InputOutput0"; - private static final String outputChannel2 = "io0/InputOutput1"; - - private static final ChannelAddress STATUS = new ChannelAddress(CTRL_ID, "Status"); - private static final ChannelAddress AWAITING_HYSTERESIS = new ChannelAddress(CTRL_ID, "AwaitingHysteresis"); - private static final ChannelAddress IO_OUTPUT_CHANNEL1 = new ChannelAddress(IO_ID, "InputOutput0"); - private static final ChannelAddress IO_OUTPUT_CHANNEL2 = new ChannelAddress(IO_ID, "InputOutput1"); - - private static final ChannelAddress SUM_GRID_ACTIVE_POWER = new ChannelAddress("_sum", "GridActivePower"); - private static final ChannelAddress SUM_ESS_SOC = new ChannelAddress("_sum", "EssSoc"); - private static final ChannelAddress SUM_ESS_DISCHARGE_POWER = new ChannelAddress("_sum", "EssDischargePower"); - @Test public void manual_undefined_test() throws Exception { - - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - new ControllerTest(new ControllerIoHeatPumpSgReadyImpl()) // - .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrHeatPump0") // .setMode(Mode.MANUAL) // .setManualState(Status.UNDEFINED) // - .setOutputChannel1(outputChannel1) // - .setOutputChannel2(outputChannel2) // + .setOutputChannel1("io0/InputOutput0") // + .setOutputChannel2("io0/InputOutput1") // .build()) .next(new TestCase() // .output(STATUS, Status.REGULAR) // - .output(IO_OUTPUT_CHANNEL1, false) // - .output(IO_OUTPUT_CHANNEL2, false)); + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false)) // + .deactivate(); } @Test public void manual_regular_test() throws Exception { - - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - new ControllerTest(new ControllerIoHeatPumpSgReadyImpl()) // - .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrHeatPump0") // .setMode(Mode.MANUAL) // .setManualState(Status.REGULAR) // - .setOutputChannel1(outputChannel1) // - .setOutputChannel2(outputChannel2) // + .setOutputChannel1("io0/InputOutput0") // + .setOutputChannel2("io0/InputOutput1") // .build()) .next(new TestCase() // .output(STATUS, Status.REGULAR) // - .output(IO_OUTPUT_CHANNEL1, false) // - .output(IO_OUTPUT_CHANNEL2, false)); + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false)) // + .deactivate(); } @Test public void manual_recommendation_test() throws Exception { - - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - new ControllerTest(new ControllerIoHeatPumpSgReadyImpl()) // - .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrHeatPump0") // .setMode(Mode.MANUAL) // .setManualState(Status.RECOMMENDATION) // - .setOutputChannel1(outputChannel1) // - .setOutputChannel2(outputChannel2) // + .setOutputChannel1("io0/InputOutput0") // + .setOutputChannel2("io0/InputOutput1") // .build()) .next(new TestCase() // .output(STATUS, Status.RECOMMENDATION) // - .output(IO_OUTPUT_CHANNEL1, false) // - .output(IO_OUTPUT_CHANNEL2, true)); + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, true)) // + .deactivate(); } @Test public void manual_force_on_test() throws Exception { - - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - new ControllerTest(new ControllerIoHeatPumpSgReadyImpl()) // - .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrHeatPump0") // .setMode(Mode.MANUAL) // .setManualState(Status.FORCE_ON) // - .setOutputChannel1(outputChannel1) // - .setOutputChannel2(outputChannel2) // + .setOutputChannel1("io0/InputOutput0") // + .setOutputChannel2("io0/InputOutput1") // .build()) .next(new TestCase() // .output(STATUS, Status.FORCE_ON) // - .output(IO_OUTPUT_CHANNEL1, true) // - .output(IO_OUTPUT_CHANNEL2, true)); + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, true)) // + .deactivate(); } @Test public void manual_lock_test() throws Exception { - - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - new ControllerTest(new ControllerIoHeatPumpSgReadyImpl()) // - .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrHeatPump0") // .setMode(Mode.MANUAL) // .setManualState(Status.LOCK) // - .setOutputChannel1(outputChannel1) // - .setOutputChannel2(outputChannel2) // + .setOutputChannel1("io0/InputOutput0") // + .setOutputChannel2("io0/InputOutput1") // .build()) .next(new TestCase() // .output(STATUS, Status.LOCK) // - .output(IO_OUTPUT_CHANNEL1, true) // - .output(IO_OUTPUT_CHANNEL2, false)); + .output("io0", INPUT_OUTPUT0, true) // + .output("io0", INPUT_OUTPUT1, false)) // + .deactivate(); } @Test public void automatic_regular_test() throws Exception { - - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - new ControllerTest(new ControllerIoHeatPumpSgReadyImpl()) // - .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrHeatPump0") // .setMode(Mode.AUTOMATIC) // .setAutomaticForceOnCtrlEnabled(false) // .setAutomaticRecommendationCtrlEnabled(false) // .setAutomaticLockCtrlEnabled(false) // - .setOutputChannel1(outputChannel1) // - .setOutputChannel2(outputChannel2) // + .setOutputChannel1("io0/InputOutput0") // + .setOutputChannel2("io0/InputOutput1") // .build()) .next(new TestCase() // .output(STATUS, Status.REGULAR) // - .output(IO_OUTPUT_CHANNEL1, false) // - .output(IO_OUTPUT_CHANNEL2, false)); + .output("io0", INPUT_OUTPUT0, false) // + .output("io0", INPUT_OUTPUT1, false)) // + .deactivate(); } @Test public void automatic_normal_config_test() throws Exception { - - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - new ControllerTest(new ControllerIoHeatPumpSgReadyImpl()) // - .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrHeatPump0") // .setMode(Mode.AUTOMATIC) // .setAutomaticRecommendationCtrlEnabled(true) // .setAutomaticRecommendationSurplusPower(3000) // @@ -192,59 +161,57 @@ public void automatic_normal_config_test() throws Exception { .setAutomaticLockCtrlEnabled(true) // .setAutomaticLockGridBuyPower(5000) // .setAutomaticLockSoc(20) // - .setOutputChannel1(outputChannel1) // - .setOutputChannel2(outputChannel2) // + .setOutputChannel1("io0/InputOutput0") // + .setOutputChannel2("io0/InputOutput1") // .setMinimumSwitchingTime(0) // .build()) .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, -4000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .input(GRID_ACTIVE_POWER, -4000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.RECOMMENDATION)) // .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, -3000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .input(GRID_ACTIVE_POWER, -3000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.FORCE_ON)) // .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, 500) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .input(GRID_ACTIVE_POWER, 500) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.RECOMMENDATION)) // .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, -2700) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .input(GRID_ACTIVE_POWER, -2700) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.FORCE_ON)) // .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, -150) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 88) // + .input(GRID_ACTIVE_POWER, -150) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 88) // .output(STATUS, Status.RECOMMENDATION)) // .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, 500) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 88) // + .input(GRID_ACTIVE_POWER, 500) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 88) // .output(STATUS, Status.REGULAR)) .next(new TestCase() // - .input(SUM_GRID_ACTIVE_POWER, 5500) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 19) // - .output(STATUS, Status.LOCK)); + .input(GRID_ACTIVE_POWER, 5500) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 19) // + .output(STATUS, Status.LOCK)) // + .deactivate(); } @Test public void automatic_switching_time_test() throws Exception { - - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - + final var clock = createDummyClock(); new ControllerTest(new ControllerIoHeatPumpSgReadyImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrHeatPump0") // .setMode(Mode.AUTOMATIC) // .setAutomaticRecommendationCtrlEnabled(true) // .setAutomaticRecommendationSurplusPower(3000) // @@ -254,64 +221,62 @@ public void automatic_switching_time_test() throws Exception { .setAutomaticLockCtrlEnabled(true) // .setAutomaticLockGridBuyPower(5000) // .setAutomaticLockSoc(20) // - .setOutputChannel1(outputChannel1) // - .setOutputChannel2(outputChannel2) // + .setOutputChannel1("io0/InputOutput0") // + .setOutputChannel2("io0/InputOutput1") // .setMinimumSwitchingTime(60) // .build()) .next(new TestCase("Test 1") // - .input(SUM_GRID_ACTIVE_POWER, -4000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .input(GRID_ACTIVE_POWER, -4000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.RECOMMENDATION)) // .next(new TestCase("Test 1") // - .timeleap(clock, 18, ChronoUnit.SECONDS) // + .timeleap(clock, 18, SECONDS) // .output(AWAITING_HYSTERESIS, true)) // .next(new TestCase("Test 2") // - .timeleap(clock, 18, ChronoUnit.SECONDS) // + .timeleap(clock, 18, SECONDS) // .output(AWAITING_HYSTERESIS, true)) // .next(new TestCase("Test 3") // - .timeleap(clock, 18, ChronoUnit.SECONDS) // + .timeleap(clock, 18, SECONDS) // .output(AWAITING_HYSTERESIS, true)) // .next(new TestCase("Test 4") // - .timeleap(clock, 18, ChronoUnit.SECONDS) // + .timeleap(clock, 18, SECONDS) // .output(AWAITING_HYSTERESIS, false)) .next(new TestCase("Test 5") // - .input(SUM_GRID_ACTIVE_POWER, -4000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .input(GRID_ACTIVE_POWER, -4000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.FORCE_ON)) // .next(new TestCase("Test 6") // - .timeleap(clock, 30, ChronoUnit.SECONDS) // + .timeleap(clock, 30, SECONDS) // .output(STATUS, Status.FORCE_ON) // .output(AWAITING_HYSTERESIS, true)) // .next(new TestCase("Test 7") // - .timeleap(clock, 10, ChronoUnit.SECONDS) // - .input(SUM_GRID_ACTIVE_POWER, -500) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .timeleap(clock, 10, SECONDS) // + .input(GRID_ACTIVE_POWER, -500) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.FORCE_ON) // .output(AWAITING_HYSTERESIS, true)) // .next(new TestCase("Test 8") // - .timeleap(clock, 30, ChronoUnit.SECONDS) // - .input(SUM_GRID_ACTIVE_POWER, 500) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .timeleap(clock, 30, SECONDS) // + .input(GRID_ACTIVE_POWER, 500) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(AWAITING_HYSTERESIS, false) // - .output(STATUS, Status.RECOMMENDATION) // - ); + .output(STATUS, Status.RECOMMENDATION)) // + .deactivate(); } @Test public void automatic_switching2_time_test() throws Exception { - - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800), ZoneOffset.UTC); - - new ControllerTest(new ControllerIoHeatPumpSgReadyImpl()).addReference("componentManager", // - new DummyComponentManager(clock)) // + final var clock = createDummyClock(); + new ControllerTest(new ControllerIoHeatPumpSgReadyImpl())// + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("sum", new DummySum()) // - .addComponent(new DummyInputOutput(IO_ID)) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrHeatPump0") // .setMode(Mode.AUTOMATIC) // .setAutomaticRecommendationCtrlEnabled(true) // .setAutomaticRecommendationSurplusPower(3000) // @@ -321,63 +286,64 @@ public void automatic_switching2_time_test() throws Exception { .setAutomaticLockCtrlEnabled(true) // .setAutomaticLockGridBuyPower(5000) // .setAutomaticLockSoc(20) // - .setOutputChannel1(outputChannel1) // - .setOutputChannel2(outputChannel2) // + .setOutputChannel1("io0/InputOutput0") // + .setOutputChannel2("io0/InputOutput1") // .setMinimumSwitchingTime(60) // .build()) .next(new TestCase("Test 1") // - .input(SUM_GRID_ACTIVE_POWER, -4000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .input(GRID_ACTIVE_POWER, -4000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.RECOMMENDATION)) // .next(new TestCase("Test 2") // - .timeleap(clock, 50, ChronoUnit.SECONDS)// - .input(SUM_GRID_ACTIVE_POWER, -3000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .timeleap(clock, 50, SECONDS)// + .input(GRID_ACTIVE_POWER, -3000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.RECOMMENDATION) // .output(AWAITING_HYSTERESIS, true)) // .next(new TestCase("Test 3") // - .timeleap(clock, 15, ChronoUnit.SECONDS)// - .input(SUM_GRID_ACTIVE_POWER, -3000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .timeleap(clock, 15, SECONDS)// + .input(GRID_ACTIVE_POWER, -3000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(AWAITING_HYSTERESIS, false)) // .next(new TestCase("Test 3 - Results") // .output(AWAITING_HYSTERESIS, true) // .output(STATUS, Status.FORCE_ON)) // .next(new TestCase("Test 4") // - .timeleap(clock, 65, ChronoUnit.SECONDS)// - .input(SUM_GRID_ACTIVE_POWER, -3000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .timeleap(clock, 65, SECONDS)// + .input(GRID_ACTIVE_POWER, -3000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(AWAITING_HYSTERESIS, false) // .output(STATUS, Status.FORCE_ON)) // .next(new TestCase("Test 5") // - .timeleap(clock, 15, ChronoUnit.SECONDS)// - .input(SUM_GRID_ACTIVE_POWER, 500) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .timeleap(clock, 15, SECONDS)// + .input(GRID_ACTIVE_POWER, 500) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(AWAITING_HYSTERESIS, false) // .output(STATUS, Status.RECOMMENDATION)) // .next(new TestCase("Test 6") // - .timeleap(clock, 65, ChronoUnit.SECONDS)// - .input(SUM_GRID_ACTIVE_POWER, 15000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 15) // + .timeleap(clock, 65, SECONDS)// + .input(GRID_ACTIVE_POWER, 15000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 15) // .output(AWAITING_HYSTERESIS, false) // .output(STATUS, Status.LOCK)) // .next(new TestCase("Test 7") // .timeleap(clock, 15, ChronoUnit.MINUTES) // - .input(SUM_GRID_ACTIVE_POWER, -2700) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 95) // + .input(GRID_ACTIVE_POWER, -2700) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 95) // .output(STATUS, Status.REGULAR)) // .next(new TestCase("Test 8") // .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(SUM_GRID_ACTIVE_POWER, -15000) // - .input(SUM_ESS_DISCHARGE_POWER, 0) // - .input(SUM_ESS_SOC, 88) // - .output(STATUS, Status.RECOMMENDATION)); // + .input(GRID_ACTIVE_POWER, -15000) // + .input(ESS_DISCHARGE_POWER, 0) // + .input(ESS_SOC, 88) // + .output(STATUS, Status.RECOMMENDATION)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.pvinverter.fixpowerlimit/test/io/openems/edge/controller/pvinverter/fixpowerlimit/ControllerPvInverterFixPowerLimitImplTest.java b/io.openems.edge.controller.pvinverter.fixpowerlimit/test/io/openems/edge/controller/pvinverter/fixpowerlimit/ControllerPvInverterFixPowerLimitImplTest.java index 03b22d8e4ce..f854cb3e17d 100644 --- a/io.openems.edge.controller.pvinverter.fixpowerlimit/test/io/openems/edge/controller/pvinverter/fixpowerlimit/ControllerPvInverterFixPowerLimitImplTest.java +++ b/io.openems.edge.controller.pvinverter.fixpowerlimit/test/io/openems/edge/controller/pvinverter/fixpowerlimit/ControllerPvInverterFixPowerLimitImplTest.java @@ -7,19 +7,16 @@ public class ControllerPvInverterFixPowerLimitImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String PV_INVERTER_ID = "pvInverter0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerPvInverterFixPowerLimitImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setPvInverterId(PV_INVERTER_ID) // + .setId("ctrl0") // + .setPvInverterId("pvInverter0") // .setPowerLimit(10000) // - .build()); // - ; + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.pvinverter.selltogridlimit/test/io/openems/edge/controller/pvinverter/selltogridlimit/ControllerPvInverterSellToGridLimitImplTest.java b/io.openems.edge.controller.pvinverter.selltogridlimit/test/io/openems/edge/controller/pvinverter/selltogridlimit/ControllerPvInverterSellToGridLimitImplTest.java index f1d89b53e25..ef3bc2313cb 100644 --- a/io.openems.edge.controller.pvinverter.selltogridlimit/test/io/openems/edge/controller/pvinverter/selltogridlimit/ControllerPvInverterSellToGridLimitImplTest.java +++ b/io.openems.edge.controller.pvinverter.selltogridlimit/test/io/openems/edge/controller/pvinverter/selltogridlimit/ControllerPvInverterSellToGridLimitImplTest.java @@ -1,153 +1,148 @@ package io.openems.edge.controller.pvinverter.selltogridlimit; +import static io.openems.common.types.OpenemsType.INTEGER; +import static io.openems.edge.common.type.TypeUtils.getAsType; +import static io.openems.edge.controller.pvinverter.selltogridlimit.ControllerPvInverterSellToGridLimitImpl.DEFAULT_MAX_ADJUSTMENT_RATE; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L1; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L2; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L3; +import static io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter.ChannelId.ACTIVE_POWER_LIMIT; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; -import io.openems.common.types.OpenemsType; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.type.TypeUtils; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.meter.test.DummyElectricityMeter; import io.openems.edge.pvinverter.test.DummyManagedSymmetricPvInverter; public class ControllerPvInverterSellToGridLimitImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String METER_ID = "meter0"; - private static final ChannelAddress GRID_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - private static final ChannelAddress GRID_ACTIVE_POWER_L1 = new ChannelAddress(METER_ID, "ActivePowerL1"); - private static final ChannelAddress GRID_ACTIVE_POWER_L2 = new ChannelAddress(METER_ID, "ActivePowerL2"); - private static final ChannelAddress GRID_ACTIVE_POWER_L3 = new ChannelAddress(METER_ID, "ActivePowerL3"); - - private static final String PV_INVERTER = "pvInverter0"; - private static final ChannelAddress PV_INVERTER_ACTIVE_POWER = new ChannelAddress(PV_INVERTER, "ActivePower"); - private static final ChannelAddress PV_INVERTER_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(PV_INVERTER, - "ActivePowerLimit"); - - private static final double ADJUST_RATE = ControllerPvInverterSellToGridLimitImpl.DEFAULT_MAX_ADJUSTMENT_RATE; - @Test public void symmetricMeterTest() throws Exception { new ControllerTest(new ControllerPvInverterSellToGridLimitImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyElectricityMeter(METER_ID)) // - .addComponent(new DummyManagedSymmetricPvInverter(PV_INVERTER)).activate(MyConfig.create() // - .setId(CTRL_ID) // - .setMeterId(METER_ID) // + .addComponent(new DummyElectricityMeter("meter0")) // + .addComponent(new DummyManagedSymmetricPvInverter("pvInverter0")) // + .activate(MyConfig.create() // + .setId("ctrl0") // + .setMeterId("meter0") // .setAsymmetricMode(false) // .setMaximumSellToGridPower(10_000) // - .setPvInverterId(PV_INVERTER) // + .setPvInverterId("pvInverter0") // .build()) .next(new TestCase() // - .input(GRID_ACTIVE_POWER, -15000) // - .input(PV_INVERTER_ACTIVE_POWER, 15000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, 10000)) // + .input("meter0", ACTIVE_POWER, -15000) // + .input("pvInverter0", ACTIVE_POWER, 15000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, 10000)) // .next(new TestCase() // - .input(GRID_ACTIVE_POWER, -15000) // - .input(PV_INVERTER_ACTIVE_POWER, 10000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, - TypeUtils.getAsType(OpenemsType.INTEGER, 10000 - 10000 * ADJUST_RATE))) // 5000 -> 8000 + .input("meter0", ACTIVE_POWER, -15000) // + .input("pvInverter0", ACTIVE_POWER, 10000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, + getAsType(INTEGER, 10000 - 10000 * DEFAULT_MAX_ADJUSTMENT_RATE))) // 5000 -> 8000 .next(new TestCase() // - .input(GRID_ACTIVE_POWER, -13000) // - .input(PV_INVERTER_ACTIVE_POWER, 8000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, - TypeUtils.getAsType(OpenemsType.INTEGER, 8000 - 8000 * ADJUST_RATE))) // 5000 -> 6400 + .input("meter0", ACTIVE_POWER, -13000) // + .input("pvInverter0", ACTIVE_POWER, 8000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, + getAsType(INTEGER, 8000 - 8000 * DEFAULT_MAX_ADJUSTMENT_RATE))) // 5000 -> 6400 .next(new TestCase() // - .input(GRID_ACTIVE_POWER, -11400) // - .input(PV_INVERTER_ACTIVE_POWER, 6400) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, - TypeUtils.getAsType(OpenemsType.INTEGER, 6400 - 6400 * ADJUST_RATE))) // 5000 -> 5120 + .input("meter0", ACTIVE_POWER, -11400) // + .input("pvInverter0", ACTIVE_POWER, 6400) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, + getAsType(INTEGER, 6400 - 6400 * DEFAULT_MAX_ADJUSTMENT_RATE))) // 5000 -> 5120 .next(new TestCase() // - .input(GRID_ACTIVE_POWER, -10120) // - .input(PV_INVERTER_ACTIVE_POWER, 5120) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, 5000)) // + .input("meter0", ACTIVE_POWER, -10120) // + .input("pvInverter0", ACTIVE_POWER, 5120) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, 5000)) // .next(new TestCase() // - .input(GRID_ACTIVE_POWER, -9000) // - .input(PV_INVERTER_ACTIVE_POWER, 5000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, 6000)) // + .input("meter0", ACTIVE_POWER, -9000) // + .input("pvInverter0", ACTIVE_POWER, 5000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, 6000)) // .next(new TestCase() // - .input(GRID_ACTIVE_POWER, 0) // - .input(PV_INVERTER_ACTIVE_POWER, 6000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, - TypeUtils.getAsType(OpenemsType.INTEGER, 6000 + 6000 * ADJUST_RATE))); // 16000 -> 7200 + .input("meter0", ACTIVE_POWER, 0) // + .input("pvInverter0", ACTIVE_POWER, 6000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, + getAsType(INTEGER, 6000 + 6000 * DEFAULT_MAX_ADJUSTMENT_RATE))) // 16000 -> 7200 + .deactivate(); } @Test public void asymmetricMeterTest() throws Exception { new ControllerTest(new ControllerPvInverterSellToGridLimitImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyElectricityMeter(METER_ID)) // - .addComponent(new DummyManagedSymmetricPvInverter(PV_INVERTER)).activate(MyConfig.create() // - .setId(CTRL_ID) // - .setMeterId(METER_ID) // + .addComponent(new DummyElectricityMeter("meter0")) // + .addComponent(new DummyManagedSymmetricPvInverter("pvInverter0")) // + .activate(MyConfig.create() // + .setId("ctrl0") // + .setMeterId("meter0") // .setAsymmetricMode(true) // .setMaximumSellToGridPower(4_000) // 12_000 in total - .setPvInverterId(PV_INVERTER) // + .setPvInverterId("pvInverter0") // .build()) .next(new TestCase() // - .input(GRID_ACTIVE_POWER_L1, -2000) // - .input(GRID_ACTIVE_POWER_L2, -4000) // - .input(GRID_ACTIVE_POWER_L3, -3000) // - .input(PV_INVERTER_ACTIVE_POWER, 12000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, 12000)) // + .input("meter0", ACTIVE_POWER_L1, -2000) // + .input("meter0", ACTIVE_POWER_L2, -4000) // + .input("meter0", ACTIVE_POWER_L3, -3000) // + .input("pvInverter0", ACTIVE_POWER, 12000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, 12000)) // .next(new TestCase() // - .input(GRID_ACTIVE_POWER_L1, -2000) // - .input(GRID_ACTIVE_POWER_L2, -5000) // - .input(GRID_ACTIVE_POWER_L3, -2000) // - .input(PV_INVERTER_ACTIVE_POWER, 12000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, - TypeUtils.getAsType(OpenemsType.INTEGER, 12000 - 12000 * ADJUST_RATE))) // 9000 -> 9600 + .input("meter0", ACTIVE_POWER_L1, -2000) // + .input("meter0", ACTIVE_POWER_L2, -5000) // + .input("meter0", ACTIVE_POWER_L3, -2000) // + .input("pvInverter0", ACTIVE_POWER, 12000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, + getAsType(INTEGER, 12000 - 12000 * DEFAULT_MAX_ADJUSTMENT_RATE))) // 9000 -> 9600 .next(new TestCase() // - .input(GRID_ACTIVE_POWER_L1, -1200) // - .input(GRID_ACTIVE_POWER_L2, -4200) // - .input(GRID_ACTIVE_POWER_L3, -1200) // - .input(PV_INVERTER_ACTIVE_POWER, 9600) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, 9000)) // + .input("meter0", ACTIVE_POWER_L1, -1200) // + .input("meter0", ACTIVE_POWER_L2, -4200) // + .input("meter0", ACTIVE_POWER_L3, -1200) // + .input("pvInverter0", ACTIVE_POWER, 9600) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, 9000)) // .next(new TestCase() // - .input(GRID_ACTIVE_POWER_L1, -1000) // - .input(GRID_ACTIVE_POWER_L2, -4000) // - .input(GRID_ACTIVE_POWER_L3, -1000) // - .input(PV_INVERTER_ACTIVE_POWER, 9000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, 9000)) // + .input("meter0", ACTIVE_POWER_L1, -1000) // + .input("meter0", ACTIVE_POWER_L2, -4000) // + .input("meter0", ACTIVE_POWER_L3, -1000) // + .input("pvInverter0", ACTIVE_POWER, 9000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, 9000)) // .next(new TestCase() // - .input(GRID_ACTIVE_POWER_L1, -1000) // - .input(GRID_ACTIVE_POWER_L2, -3700) // - .input(GRID_ACTIVE_POWER_L3, -1000) // - .input(PV_INVERTER_ACTIVE_POWER, 9000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, 9900)) // + .input("meter0", ACTIVE_POWER_L1, -1000) // + .input("meter0", ACTIVE_POWER_L2, -3700) // + .input("meter0", ACTIVE_POWER_L3, -1000) // + .input("pvInverter0", ACTIVE_POWER, 9000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, 9900)) // .next(new TestCase() // - .input(GRID_ACTIVE_POWER_L1, -2000) // - .input(GRID_ACTIVE_POWER_L2, -5000) // - .input(GRID_ACTIVE_POWER_L3, -2000) // - .input(PV_INVERTER_ACTIVE_POWER, 9900) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, - TypeUtils.getAsType(OpenemsType.INTEGER, 9900 - 9900 * ADJUST_RATE))) // 6900 -> 7920 + .input("meter0", ACTIVE_POWER_L1, -2000) // + .input("meter0", ACTIVE_POWER_L2, -5000) // + .input("meter0", ACTIVE_POWER_L3, -2000) // + .input("pvInverter0", ACTIVE_POWER, 9900) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, + getAsType(INTEGER, 9900 - 9900 * DEFAULT_MAX_ADJUSTMENT_RATE))) // 6900 -> 7920 .next(new TestCase() // - .input(GRID_ACTIVE_POWER_L1, -2000) // - .input(GRID_ACTIVE_POWER_L2, -5000) // - .input(GRID_ACTIVE_POWER_L3, -2000) // - .input(PV_INVERTER_ACTIVE_POWER, 7920) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, - TypeUtils.getAsType(OpenemsType.INTEGER, 7920 - 7920 * ADJUST_RATE))); // 4920 -> 6336 + .input("meter0", ACTIVE_POWER_L1, -2000) // + .input("meter0", ACTIVE_POWER_L2, -5000) // + .input("meter0", ACTIVE_POWER_L3, -2000) // + .input("pvInverter0", ACTIVE_POWER, 7920) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, + getAsType(INTEGER, 7920 - 7920 * DEFAULT_MAX_ADJUSTMENT_RATE))); // 4920 -> 6336 new ControllerTest(new ControllerPvInverterSellToGridLimitImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyElectricityMeter(METER_ID)) // - .addComponent(new DummyManagedSymmetricPvInverter(PV_INVERTER)).activate(MyConfig.create() // - .setId(CTRL_ID) // - .setMeterId(METER_ID) // + .addComponent(new DummyElectricityMeter("meter0")) // + .addComponent(new DummyManagedSymmetricPvInverter("pvInverter0")) // + .activate(MyConfig.create() // + .setId("ctrl0") // + .setMeterId("meter0") // .setAsymmetricMode(true) // .setMaximumSellToGridPower(4_000) // 12_000 in total - .setPvInverterId(PV_INVERTER) // + .setPvInverterId("pvInverter0") // .build()) .next(new TestCase() // - .input(GRID_ACTIVE_POWER_L1, 1000) // - .input(GRID_ACTIVE_POWER_L2, 2000) // - .input(GRID_ACTIVE_POWER_L3, 3000) // - .input(PV_INVERTER_ACTIVE_POWER, 1000) // - .output(PV_INVERTER_SET_ACTIVE_POWER_EQUALS, 16000)) // - ; + .input("meter0", ACTIVE_POWER_L1, 1000) // + .input("meter0", ACTIVE_POWER_L2, 2000) // + .input("meter0", ACTIVE_POWER_L3, 3000) // + .input("pvInverter0", ACTIVE_POWER, 1000) // + .output("pvInverter0", ACTIVE_POWER_LIMIT, 16000)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.symmetric.balancingschedule/test/io/openems/edge/controller/symmetric/balancingschedule/ControllerEssBalancingScheduleImplTest.java b/io.openems.edge.controller.symmetric.balancingschedule/test/io/openems/edge/controller/symmetric/balancingschedule/ControllerEssBalancingScheduleImplTest.java index 7501e1d9d60..ed88ef4c8be 100644 --- a/io.openems.edge.controller.symmetric.balancingschedule/test/io/openems/edge/controller/symmetric/balancingschedule/ControllerEssBalancingScheduleImplTest.java +++ b/io.openems.edge.controller.symmetric.balancingschedule/test/io/openems/edge/controller/symmetric/balancingschedule/ControllerEssBalancingScheduleImplTest.java @@ -1,39 +1,31 @@ package io.openems.edge.controller.symmetric.balancingschedule; +import static io.openems.common.utils.JsonUtils.buildJsonArray; +import static io.openems.common.utils.JsonUtils.buildJsonObject; +import static io.openems.edge.controller.symmetric.balancingschedule.ControllerEssBalancingSchedule.ChannelId.GRID_ACTIVE_POWER_SET_POINT; +import static io.openems.edge.controller.symmetric.balancingschedule.ControllerEssBalancingSchedule.ChannelId.NO_ACTIVE_SETPOINT; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS_WITH_PID; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.GRID_MODE; +import static java.time.temporal.ChronoUnit.SECONDS; + import java.time.Instant; import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; import org.junit.Test; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; -import io.openems.common.utils.JsonUtils; import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.test.DummyElectricityMeter; public class ControllerEssBalancingScheduleImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - private static final String METER_ID = "meter0"; - - private static final ChannelAddress CTRL_NO_ACTIVE_SETPOINT = new ChannelAddress(CTRL_ID, "NoActiveSetpoint"); - private static final ChannelAddress CTRL_GRID_ACTIVE_POWER_SET_POINT = new ChannelAddress(CTRL_ID, - "GridActivePowerSetPoint"); - - private static final ChannelAddress ESS_GRID_MODE = new ChannelAddress(ESS_ID, "GridMode"); - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); - private static final ChannelAddress SET_ACTIVE_POWER_EQUALS_WITH_PID = new ChannelAddress(ESS_ID, - "SetActivePowerEqualsWithPid"); - - private static final ChannelAddress GRID_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - @Test public void test() throws Exception { final var start = 1577836800L; @@ -42,47 +34,47 @@ public void test() throws Exception { new ControllerTest(new ControllerEssBalancingScheduleImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // - .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("ess", new DummyManagedSymmetricEss("ess0")) // + .addReference("meter", new DummyElectricityMeter("meter0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // - .setSchedule(JsonUtils.buildJsonArray()// - .add(JsonUtils.buildJsonObject()// + .setId("ctrl0") // + .setEssId("ess0") // + .setMeterId("meter0") // + .setSchedule(buildJsonArray()// + .add(buildJsonObject()// .addProperty("startTimestamp", start + 500) // .addProperty("duration", 900) // .addProperty("activePowerSetPoint", 0) // .build()) // - .add(JsonUtils.buildJsonObject()// + .add(buildJsonObject()// .addProperty("startTimestamp", start + 500 + 800) // .addProperty("duration", 900) // .addProperty("activePowerSetPoint", 3000) // - .build() // - ).build().toString() // - ).build()) // + .build()) // + .build().toString()) // + .build()) // .next(new TestCase("No active setpoint") // - .input(ESS_GRID_MODE, GridMode.ON_GRID) // - .input(GRID_ACTIVE_POWER, 4000) // - .input(ESS_ACTIVE_POWER, 1000) // - .output(CTRL_NO_ACTIVE_SETPOINT, true)) // + .input("ess0", GRID_MODE, GridMode.ON_GRID) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 4000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 1000) // + .output(NO_ACTIVE_SETPOINT, true)) // .next(new TestCase("Balance to 0") // - .timeleap(clock, 500, ChronoUnit.SECONDS) // - .input(GRID_ACTIVE_POWER, 4000) // - .input(ESS_ACTIVE_POWER, 1000) // - .output(CTRL_NO_ACTIVE_SETPOINT, false) // - .output(SET_ACTIVE_POWER_EQUALS_WITH_PID, 5000)) // + .timeleap(clock, 500, SECONDS) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 4000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 1000) // + .output(NO_ACTIVE_SETPOINT, false) // + .output("ess0", SET_ACTIVE_POWER_EQUALS_WITH_PID, 5000)) // .next(new TestCase("Balance to -2000 via Channel") // - .input(CTRL_GRID_ACTIVE_POWER_SET_POINT, -2000) // - .input(GRID_ACTIVE_POWER, 4000) // - .input(ESS_ACTIVE_POWER, 1000) // - .output(SET_ACTIVE_POWER_EQUALS_WITH_PID, 7000)) // + .input(GRID_ACTIVE_POWER_SET_POINT, -2000) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 4000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 1000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS_WITH_PID, 7000)) // .next(new TestCase("Balance to 3000") // - .timeleap(clock, 800, ChronoUnit.SECONDS) // - .input(GRID_ACTIVE_POWER, 4000) // - .input(ESS_ACTIVE_POWER, 1000) // - .output(CTRL_NO_ACTIVE_SETPOINT, false) // - .output(SET_ACTIVE_POWER_EQUALS_WITH_PID, 2000)) // - ; + .timeleap(clock, 800, SECONDS) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 4000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 1000) // + .output(NO_ACTIVE_SETPOINT, false) // + .output("ess0", SET_ACTIVE_POWER_EQUALS_WITH_PID, 2000)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.symmetric.fixreactivepower/test/io/openems/edge/controller/symmetric/fixreactivepower/ControllerEssFixReactivePowerImplTest.java b/io.openems.edge.controller.symmetric.fixreactivepower/test/io/openems/edge/controller/symmetric/fixreactivepower/ControllerEssFixReactivePowerImplTest.java index 9ffd8bebe22..f576936930b 100644 --- a/io.openems.edge.controller.symmetric.fixreactivepower/test/io/openems/edge/controller/symmetric/fixreactivepower/ControllerEssFixReactivePowerImplTest.java +++ b/io.openems.edge.controller.symmetric.fixreactivepower/test/io/openems/edge/controller/symmetric/fixreactivepower/ControllerEssFixReactivePowerImplTest.java @@ -7,19 +7,16 @@ public class ControllerEssFixReactivePowerImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerEssFixReactivePowerImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setPower(1000) // - .build()); // - ; + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.symmetric.limitactivepower/test/io/openems/edge/controller/symmetric/limitactivepower/ControllerEssLimitActivePowerImplTest.java b/io.openems.edge.controller.symmetric.limitactivepower/test/io/openems/edge/controller/symmetric/limitactivepower/ControllerEssLimitActivePowerImplTest.java index c9261c90ed3..ace177e6000 100644 --- a/io.openems.edge.controller.symmetric.limitactivepower/test/io/openems/edge/controller/symmetric/limitactivepower/ControllerEssLimitActivePowerImplTest.java +++ b/io.openems.edge.controller.symmetric.limitactivepower/test/io/openems/edge/controller/symmetric/limitactivepower/ControllerEssLimitActivePowerImplTest.java @@ -7,21 +7,18 @@ public class ControllerEssLimitActivePowerImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerEssLimitActivePowerImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setMaxChargePower(1000) // .setMaxDischargePower(1000) // .setValidatePowerConstraints(false) // - .build()); // - ; + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.symmetric.peakshaving/test/io/openems/edge/controller/symmetric/peakshaving/ControllerEssPeakShavingImplTest.java b/io.openems.edge.controller.symmetric.peakshaving/test/io/openems/edge/controller/symmetric/peakshaving/ControllerEssPeakShavingImplTest.java index b1af67695a4..5249a7598aa 100644 --- a/io.openems.edge.controller.symmetric.peakshaving/test/io/openems/edge/controller/symmetric/peakshaving/ControllerEssPeakShavingImplTest.java +++ b/io.openems.edge.controller.symmetric.peakshaving/test/io/openems/edge/controller/symmetric/peakshaving/ControllerEssPeakShavingImplTest.java @@ -1,97 +1,91 @@ package io.openems.edge.controller.symmetric.peakshaving; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.ess.test.DummyPower; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.test.DummyElectricityMeter; public class ControllerEssPeakShavingImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String ESS_ID = "ess0"; - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - - private static final String METER_ID = "meter0"; - private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - @Test public void test() throws Exception { new ControllerTest(new ControllerEssPeakShavingImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID) // + .addComponent(new DummyManagedSymmetricEss("ess0") // .setPower(new DummyPower(0.3, 0.3, 0.1))) // - .addComponent(new DummyElectricityMeter(METER_ID)) // + .addComponent(new DummyElectricityMeter("meter0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .setId("ctrl0") // + .setEssId("ess0") // + .setMeterId("meter0") // .setPeakShavingPower(100_000) // .setRechargePower(50_000) // .build()) .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, 120000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 6000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 6000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, 120000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 12000)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 12000)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 3793) // - .input(METER_ACTIVE_POWER, 120000 - 3793) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 16483)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 3793) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 3793) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 16483)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 8981) // - .input(METER_ACTIVE_POWER, 120000 - 8981) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19649)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 8981) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 8981) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19649)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 13723) // - .input(METER_ACTIVE_POWER, 120000 - 13723) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 21577)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 13723) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 13723) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 21577)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 17469) // - .input(METER_ACTIVE_POWER, 120000 - 17469) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 22436)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 17469) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 17469) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 22436)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 20066) // - .input(METER_ACTIVE_POWER, 120000 - 20066) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 22531)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 20066) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 20066) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 22531)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 21564) // - .input(METER_ACTIVE_POWER, 120000 - 21564) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 22171)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 21564) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 21564) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 22171)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 22175) // - .input(METER_ACTIVE_POWER, 120000 - 22175) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 21608)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 22175) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 22175) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 21608)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 22173) // - .input(METER_ACTIVE_POWER, 120000 - 22173) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 21017)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 22173) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 22173) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 21017)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 21816) // - .input(METER_ACTIVE_POWER, 120000 - 21816) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 20508)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 21816) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 21816) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 20508)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 21311) // - .input(METER_ACTIVE_POWER, 120000 - 21311) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 20129)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 21311) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 21311) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 20129)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 20803) // - .input(METER_ACTIVE_POWER, 120000 - 20803) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19889)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 20803) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 20803) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19889)) // .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 20377) // - .input(METER_ACTIVE_POWER, 120000 - 20377) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 19767)); // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 20377) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000 - 20377) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 19767)) // + .deactivate(); } } diff --git a/io.openems.edge.controller.symmetric.randompower/test/io/openems/edge/controller/symmetric/randompower/ControllerEssRandomPowerImplTest.java b/io.openems.edge.controller.symmetric.randompower/test/io/openems/edge/controller/symmetric/randompower/ControllerEssRandomPowerImplTest.java index 94172daab79..ab9b1bfd8f3 100644 --- a/io.openems.edge.controller.symmetric.randompower/test/io/openems/edge/controller/symmetric/randompower/ControllerEssRandomPowerImplTest.java +++ b/io.openems.edge.controller.symmetric.randompower/test/io/openems/edge/controller/symmetric/randompower/ControllerEssRandomPowerImplTest.java @@ -7,19 +7,17 @@ public class ControllerEssRandomPowerImplTest { - private static final String CTRL_ID = "ctrl0"; - private static final String ESS_ID = "ess0"; - @Test public void test() throws Exception { new ControllerTest(new ControllerEssRandomPowerImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // + .setId("ctrl0") // + .setEssId("ess0") // .setMinPower(0) // .setMaxPower(1000) // - .build()); // + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.controller.symmetric.timeslotpeakshaving/test/io/openems/edge/controller/timeslotpeakshaving/ControllerEssTimeslotPeakshavingImplTest.java b/io.openems.edge.controller.symmetric.timeslotpeakshaving/test/io/openems/edge/controller/timeslotpeakshaving/ControllerEssTimeslotPeakshavingImplTest.java index f8f7066afcf..fb6cb7828ec 100644 --- a/io.openems.edge.controller.symmetric.timeslotpeakshaving/test/io/openems/edge/controller/timeslotpeakshaving/ControllerEssTimeslotPeakshavingImplTest.java +++ b/io.openems.edge.controller.symmetric.timeslotpeakshaving/test/io/openems/edge/controller/timeslotpeakshaving/ControllerEssTimeslotPeakshavingImplTest.java @@ -1,47 +1,40 @@ package io.openems.edge.controller.timeslotpeakshaving; +import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; +import static io.openems.edge.ess.api.SymmetricEss.ChannelId.SOC; +import static java.time.temporal.ChronoUnit.MINUTES; + import java.time.Instant; import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; import org.junit.Test; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.ess.test.DummyPower; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.test.DummyElectricityMeter; public class ControllerEssTimeslotPeakshavingImplTest { - private static final String CTRL_ID = "ctrl0"; - - private static final String ESS_ID = "ess0"; - private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); - private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); - private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - - private static final String METER_ID = "meter0"; - private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - @Test public void test() throws Exception { final var clock = new TimeLeapClock(Instant.parse("2020-02-03T08:30:00.00Z"), ZoneOffset.UTC); new ControllerTest(new ControllerEssTimeslotPeakshavingImpl()) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID) // + .addComponent(new DummyManagedSymmetricEss("ess0") // .setPower(new DummyPower(0.3, 0.3, 0.1)) // .withGridMode(GridMode.ON_GRID)) // - .addComponent(new DummyElectricityMeter(METER_ID)) // + .addComponent(new DummyElectricityMeter("meter0")) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .setId("ctrl0") // + .setEssId("ess0") // + .setMeterId("meter0") // .setPeakShavingPower(100_000) // .setRechargePower(50_000) // .setSlowChargePower(50_000) // @@ -60,34 +53,37 @@ public void test() throws Exception { .setSunday(true) // .build()) .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(ESS_SOC, 90) // - .input(METER_ACTIVE_POWER, 120000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, null)) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 0) // + .input("ess0", SOC, 90) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, null)) // .next(new TestCase() // - .timeleap(clock, 31, ChronoUnit.MINUTES)/* current time is 09:31, run in slow charge state */ - .input(ESS_SOC, 96) // - .input(ESS_ACTIVE_POWER, 5000) // - .input(METER_ACTIVE_POWER, 120000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 13500)) // + .timeleap(clock, 31, MINUTES)/* current time is 09:31, run in slow charge state */ + .input("ess0", SOC, 96) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 5000) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 13500)) // .next(new TestCase() // - .timeleap(clock, 31, ChronoUnit.MINUTES)/* current time is 09:31, run in hysterisis state */ - .input(ESS_SOC, 100) // - .input(ESS_ACTIVE_POWER, 5000) // - .input(METER_ACTIVE_POWER, 120000)) // nothing set on, Ess's setActivePower + .timeleap(clock, 31, MINUTES)/* current time is 09:31, run in hysterisis state */ + .input("ess0", SOC, 100) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 5000) // + // nothing set on, Ess's setActivePower + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000)) .next(new TestCase() // - .input(ESS_SOC, 94) // - .input(ESS_ACTIVE_POWER, 5000) // - .input(METER_ACTIVE_POWER, 120000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 27000)) // + .input("ess0", SOC, 94) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 5000) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 27000)) // .next(new TestCase() // - .timeleap(clock, 75, ChronoUnit.MINUTES)/* current time is 10:47, run in high threshold state */ - .input(ESS_ACTIVE_POWER, 5000) // - .input(METER_ACTIVE_POWER, 120000) // - .output(ESS_SET_ACTIVE_POWER_EQUALS, 33000)) // + .timeleap(clock, 75, MINUTES)/* current time is 10:47, run in high threshold state */ + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 5000) // + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000) // + .output("ess0", SET_ACTIVE_POWER_EQUALS, 33000)) // .next(new TestCase() // - .timeleap(clock, 75, ChronoUnit.MINUTES)/* current time is 12:02 run in normal state */ - .input(ESS_ACTIVE_POWER, 5000) // - .input(METER_ACTIVE_POWER, 120000)); // nothing set on, Ess's setActivePower + .timeleap(clock, 75, MINUTES)/* current time is 12:02 run in normal state */ + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 5000) // + // nothing set on, Ess's setActivePower + .input("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 120000)) // + .deactivate(); } } diff --git a/io.openems.edge.core/src/io/openems/edge/app/common/props/ComponentProps.java b/io.openems.edge.core/src/io/openems/edge/app/common/props/ComponentProps.java index f524bc82f68..4415c1ef83a 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/common/props/ComponentProps.java +++ b/io.openems.edge.core/src/io/openems/edge/app/common/props/ComponentProps.java @@ -11,6 +11,7 @@ import com.google.gson.JsonNull; import com.google.gson.JsonPrimitive; +import io.openems.common.types.MeterType; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.core.appmanager.AppDef; @@ -31,7 +32,6 @@ import io.openems.edge.core.appmanager.formly.enums.DisplayType; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; /** * Static method collection for {@link AppDef AppDefs} for selecting different diff --git a/io.openems.edge.core/src/io/openems/edge/app/enums/MeterType.java b/io.openems.edge.core/src/io/openems/edge/app/enums/MeterType.java index 5f19565f139..f856cf3b2d7 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/enums/MeterType.java +++ b/io.openems.edge.core/src/io/openems/edge/app/enums/MeterType.java @@ -5,7 +5,7 @@ import io.openems.edge.core.appmanager.TranslationUtil; /** - * Copy of {@link io.openems.edge.meter.api.MeterType}. + * Copy of {@link io.openems.common.types.MeterType}. */ public enum MeterType implements TranslatableEnum { PRODUCTION("App.Meter.production"), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsProps.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsProps.java index 258c66c960f..1904864db00 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsProps.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsProps.java @@ -1,7 +1,9 @@ package io.openems.edge.app.evcs; +import static io.openems.edge.app.common.props.CommonProps.defaultDef; import static io.openems.edge.core.appmanager.formly.enums.InputType.NUMBER; +import java.util.Arrays; import java.util.List; import java.util.stream.IntStream; @@ -11,7 +13,6 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.session.Language; import io.openems.common.utils.JsonUtils; -import io.openems.edge.app.common.props.CommonProps; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.core.appmanager.AppDef; @@ -28,6 +29,7 @@ import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; import io.openems.edge.core.appmanager.formly.builder.FieldGroupBuilder; import io.openems.edge.core.appmanager.formly.enums.DisplayType; +import io.openems.edge.evcs.api.PhaseRotation; public final class EvcsProps { @@ -45,7 +47,7 @@ private EvcsProps() { public static AppDef numberOfChargePoints(// final int maxValue // ) { - return AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def // + return AppDef.copyOfGeneric(defaultDef(), def -> def // .setTranslatedLabel("App.Evcs.numberOfChargingStations.label") // .setDefaultValue(1) // .setField(JsonFormlyUtil::buildSelectFromNameable, (app, property, l, parameter, field) -> // @@ -103,7 +105,7 @@ private static void field(// */ public static AppDef clusterMaxHardwarePower( Nameable acceptProperty) { - return AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def // + return AppDef.copyOfGeneric(defaultDef(), def -> def // .setTranslatedLabel("App.Evcs.Cluster.maxChargeFromGrid.label") // .setAllowedToSave(false) // .setIsAllowedToSee((app, property, l, parameter, user) -> { @@ -182,4 +184,20 @@ private static final boolean isClusterInstalled(ComponentManager componentManage return false; } + /** + * Creates a {@link AppDef} for a {@link PhaseRotation}. + * + * @return the {@link AppDef} + */ + public static final AppDef phaseRotation() { + return AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabel("App.Evcs.phaseRotation.label") // + .setTranslatedDescription("App.Evcs.phaseRotation.description") // + .setDefaultValue(PhaseRotation.L1_L2_L3) // + .setField(JsonFormlyUtil::buildSelectFromNameable, (app, property, l, parameter, field) -> { + field.setOptions(Arrays.stream(PhaseRotation.values()) // + .map(PhaseRotation::name) // + .toList()); + })); + } } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java index ed993465b63..59586a5769e 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java @@ -73,6 +73,7 @@ "EVCS_ID": "evcs0", "CTRL_EVCS_ID": "ctrlEvcs0", "IP": "192.168.25.30", + "PHASE_ROTATION":"L1_L2_L3", "NUMBER_OF_CHARGING_STATIONS": 1, "EVCS_ID_CP_2": "evcs0", "CTRL_EVCS_ID_CP_2": "ctrlEvcs0", @@ -144,6 +145,7 @@ public static enum Property implements PropertyParent { .collect(Exp.toArrayExpression()) // .every(i -> Exp.currentModelValue(EVCS_ID).notEqual(i)))); }))), // + PHASE_ROTATION(AppDef.copyOfGeneric(EvcsProps.phaseRotation())), // ; private final AppDef def; @@ -317,6 +319,7 @@ OpenemsNamedException> appPropertyConfigurationFactory() { final var alias = this.getString(p, l, SubPropertyFirstChargepoint.ALIAS); final var ip = this.getString(p, l, SubPropertyFirstChargepoint.IP); final var evcsId = this.getId(t, p, Property.EVCS_ID); + final var phaseRotation = this.getString(p, l, Property.PHASE_ROTATION); final var ctrlEvcsId = this.getId(t, p, Property.CTRL_EVCS_ID); schedulerIds.add(new SchedulerComponent(ctrlEvcsId, "Controller.Evcs", this.getAppId())); @@ -324,6 +327,7 @@ OpenemsNamedException> appPropertyConfigurationFactory() { final var components = Lists.newArrayList(// new EdgeConfig.Component(evcsId, alias, factorieId, JsonUtils.buildJsonObject() // .addProperty("ip", ip) // + .addPropertyIfNotNull("phaseRotation", phaseRotation) // .build()), // new EdgeConfig.Component(ctrlEvcsId, controllerAlias, "Controller.Evcs", JsonUtils.buildJsonObject() // .addProperty("evcs.id", evcsId) // @@ -339,6 +343,7 @@ OpenemsNamedException> appPropertyConfigurationFactory() { components.add(new EdgeConfig.Component(evcsIdCp2, aliasCp2, factorieId, JsonUtils.buildJsonObject() // .addProperty("ip", ipCp2) // + .addPropertyIfNotNull("phaseRotation", phaseRotation) // .build())); components.add(new EdgeConfig.Component(ctrlEvcsIdCp2, controllerAlias, "Controller.Evcs", JsonUtils.buildJsonObject() // diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java index e9a680cb831..2ab629b5ae7 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java @@ -55,7 +55,8 @@ "properties":{ "EVCS_ID": "evcs0", "CTRL_EVCS_ID": "ctrlEvcs0", - "IP":"192.168.25.11" + "IP":"192.168.25.11", + "PHASE_ROTATION":"L1_L2_L3" }, "appDescriptor": { "websiteUrl": {@link AppDescriptor#getWebsiteUrl()} @@ -80,6 +81,7 @@ public enum Property implements Type def; @@ -120,6 +122,7 @@ protected ThrowingTriFunction, L // values the user enters final var ip = this.getString(p, l, Property.IP); final var alias = this.getString(p, l, Property.ALIAS); + final var phaseRotation = this.getString(p, l, Property.PHASE_ROTATION); // values which are being auto generated by the appmanager final var evcsId = this.getId(t, p, Property.EVCS_ID); @@ -133,6 +136,7 @@ protected ThrowingTriFunction, L var components = Lists.newArrayList(// new EdgeConfig.Component(evcsId, alias, "Evcs.Keba.KeContact", JsonUtils.buildJsonObject() // .addPropertyIfNotNull("ip", ip) // + .addPropertyIfNotNull("phaseRotation", phaseRotation) // .build()), // new EdgeConfig.Component(ctrlEvcsId, controllerAlias, "Controller.Evcs", JsonUtils.buildJsonObject() // .addProperty("evcs.id", evcsId) // diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHomeComponents.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHomeComponents.java index 72f62c234ba..a70a3242f1e 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHomeComponents.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHomeComponents.java @@ -35,6 +35,24 @@ public static EdgeConfig.Component battery(// final ResourceBundle bundle, // final String batteryId, // final String modbusIdInternal // + ) { + return battery(bundle, batteryId, modbusIdInternal, "AUTO"); + } + + /** + * Creates a default battery component for a FENECON Home. + * + * @param bundle the translation bundle + * @param batteryId the id of the battery + * @param modbusIdInternal the id of the internal modbus bridge + * @param batteryStartStop the startStop target of the bridge + * @return the {@link Component} + */ + public static EdgeConfig.Component battery(// + final ResourceBundle bundle, // + final String batteryId, // + final String modbusIdInternal, // + final String batteryStartStop // ) { return new EdgeConfig.Component(batteryId, TranslationUtil.getTranslation(bundle, "App.IntegratedSystem.battery0.alias"), "Battery.Fenecon.Home", // @@ -43,7 +61,7 @@ public static EdgeConfig.Component battery(// .addProperty("batteryStartUpRelay", "io0/Relay4") // .addProperty("modbus.id", modbusIdInternal) // .addProperty("modbusUnitId", 1) // - .addProperty("startStop", "AUTO") // + .addProperty("startStop", batteryStartStop) // .build()); } @@ -181,7 +199,8 @@ public static EdgeConfig.Component modbusInternal(// final String modbusIdInternal // ) { return new EdgeConfig.Component(modbusIdInternal, - TranslationUtil.getTranslation(bundle, "App.IntegratedSystem.modbus0.alias"), "Bridge.Modbus.Serial", // + TranslationUtil.getTranslation(bundle, "App.IntegratedSystem.modbusToBattery.alias"), + "Bridge.Modbus.Serial", // JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("baudRate", 19200) // diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java index 31200d39881..9e0ee9fdeed 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java @@ -4,6 +4,7 @@ import static io.openems.edge.app.common.props.CommonProps.defaultDef; import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; +import static io.openems.edge.core.appmanager.validator.Checkables.checkOr; import java.util.Map; import java.util.function.Function; @@ -169,7 +170,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); + .setCompatibleCheckableConfigs(checkOr(checkHome(), checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java index 0c2f6d2f10d..5c91317ae73 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java @@ -2,6 +2,7 @@ import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; +import static io.openems.edge.core.appmanager.validator.Checkables.checkOr; import java.util.Map; import java.util.function.Function; @@ -168,7 +169,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); + .setCompatibleCheckableConfigs(checkOr(checkHome(), checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/GroupeE.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/GroupeE.java index b466c39ff10..7a868a62d1c 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/GroupeE.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/GroupeE.java @@ -2,6 +2,7 @@ import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; +import static io.openems.edge.core.appmanager.validator.Checkables.checkOr; import java.util.Map; import java.util.function.Function; @@ -151,7 +152,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); + .setCompatibleCheckableConfigs(checkOr(checkHome(), checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/RabotCharge.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/RabotCharge.java index de9a446a214..3ad6b7df740 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/RabotCharge.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/RabotCharge.java @@ -3,6 +3,7 @@ import static io.openems.edge.core.appmanager.formly.enums.InputType.PASSWORD; import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; +import static io.openems.edge.core.appmanager.validator.Checkables.checkOr; import java.util.Map; import java.util.function.Function; @@ -178,7 +179,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); + .setCompatibleCheckableConfigs(checkOr(checkHome(), checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StadtwerkHassfurt.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StadtwerkHassfurt.java index 8bf099ba335..09cd81281b6 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StadtwerkHassfurt.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StadtwerkHassfurt.java @@ -3,6 +3,7 @@ import static io.openems.edge.app.common.props.CommonProps.defaultDef; import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; +import static io.openems.edge.core.appmanager.validator.Checkables.checkOr; import java.util.Map; import java.util.function.Function; @@ -167,7 +168,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); + .setCompatibleCheckableConfigs(checkOr(checkHome(), checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java index fbbeb951333..7c4c5f96b7b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java @@ -2,6 +2,7 @@ import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; +import static io.openems.edge.core.appmanager.validator.Checkables.checkOr; import java.util.Map; import java.util.function.Function; @@ -164,7 +165,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); + .setCompatibleCheckableConfigs(checkOr(checkHome(), checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Swisspower.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Swisspower.java new file mode 100644 index 00000000000..8726789cf11 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Swisspower.java @@ -0,0 +1,197 @@ +package io.openems.edge.app.timeofusetariff; + +import static io.openems.edge.core.appmanager.formly.enums.InputType.PASSWORD; +import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; +import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.common.collect.Lists; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.CommonProps; +import io.openems.edge.app.timeofusetariff.Swisspower.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentManagerSupplier; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.Nameable; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerByCentralOrderConfiguration.SchedulerComponent; +import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; + +/** + * Describes a App for Swisspower. + * + *

+  {
+    "appId":"App.TimeOfUseTariff.Swisspower",
+    "alias":"Swisspower",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"CTRL_ESS_TIME_OF_USE_TARIFF_ID": "ctrlEssTimeOfUseTariff0",
+    	"TIME_OF_USE_TARIFF_PROVIDER_ID": "timeOfUseTariff0",
+    	"ACCESS_TOKEN": {token},
+    	"METERING_CODE": {code}
+    },
+    "appDescriptor": {
+    	"websiteUrl": {@link AppDescriptor#getWebsiteUrl()}
+    }
+  }
+ * 
+ */ +@Component(name = "App.TimeOfUseTariff.Swisspower") +public class Swisspower extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type, Nameable { + // Component-IDs + CTRL_ESS_TIME_OF_USE_TARIFF_ID(AppDef.componentId("ctrlEssTimeOfUseTariff0")), // + TIME_OF_USE_TARIFF_PROVIDER_ID(AppDef.componentId("timeOfUseTariff0")), // + + // Properties + ALIAS(CommonProps.alias()), // + ACCESS_TOKEN(AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def// + .setTranslatedLabelWithAppPrefix(".accessToken.label") // + .setTranslatedDescriptionWithAppPrefix(".accessToken.description") // + .setRequired(true) // + .setField(JsonFormlyUtil::buildInput, (app, prop, l, params, field) -> { + field.setInputType(PASSWORD); + }) // + .bidirectional(TIME_OF_USE_TARIFF_PROVIDER_ID, "accessToken", + ComponentManagerSupplier::getComponentManager, t -> { + return JsonUtils.getAsOptionalString(t) // + .map(s -> { + if (s.isEmpty()) { + return null; + } + return new JsonPrimitive("xxx"); + }) // + .orElse(null); + }))), + METERING_CODE(AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def// + .setTranslatedLabelWithAppPrefix(".meteringCode.label") // + .setTranslatedDescriptionWithAppPrefix(".meteringCode.description") // + .setRequired(true) // + .setField(JsonFormlyUtil::buildInput))); + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Property self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, Type.Parameter.BundleParameter> getParamter() { + return Type.Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public Swisspower(@Reference ComponentManager componentManager, ComponentContext context, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var ctrlEssTimeOfUseTariffId = this.getId(t, p, Property.CTRL_ESS_TIME_OF_USE_TARIFF_ID); + final var timeOfUseTariffProviderId = this.getId(t, p, Property.TIME_OF_USE_TARIFF_PROVIDER_ID); + + final var alias = this.getString(p, l, Property.ALIAS); + final var accessToken = this.getValueOrDefault(p, Property.ACCESS_TOKEN, null); + final var meteringCode = this.getString(p, l, Property.METERING_CODE); + + var components = Lists.newArrayList(// + new EdgeConfig.Component(ctrlEssTimeOfUseTariffId, alias, "Controller.Ess.Time-Of-Use-Tariff", + JsonUtils.buildJsonObject() // + .addProperty("ess.id", "ess0") // + .build()), // + new EdgeConfig.Component(timeOfUseTariffProviderId, this.getName(l), "TimeOfUseTariff.Swisspower", + JsonUtils.buildJsonObject() // + .addPropertyIfNotNull("meteringCode", meteringCode) // + .onlyIf(accessToken != null && !accessToken.equals("xxx"), b -> { + b.addProperty("accessToken", accessToken); + }) // + .build())// + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.schedulerByCentralOrder(new SchedulerComponent(ctrlEssTimeOfUseTariffId, + "Controller.Ess.Time-Of-Use-Tariff", this.getAppId()))) // + .addTask(Tasks.persistencePredictor("_sum/UnmanagedConsumptionActivePower")) // + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.TIME_OF_USE_TARIFF }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + protected ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); + } + + @Override + protected Swisspower getApp() { + return this; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java index 3eeeb094bab..608b2a4ec4b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java @@ -3,6 +3,7 @@ import static io.openems.edge.core.appmanager.formly.enums.InputType.PASSWORD; import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; +import static io.openems.edge.core.appmanager.validator.Checkables.checkOr; import java.util.Map; import java.util.function.Function; @@ -197,7 +198,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); + .setCompatibleCheckableConfigs(checkOr(checkHome(), checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java index b7c8349e4ed..87fad0519b6 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java @@ -69,7 +69,10 @@ public void aggregate(ComponentConfiguration config, ComponentConfiguration oldC if (oldConfig != null) { var componentDiff = new ArrayList<>(oldConfig.components()); if (config != null) { - componentDiff.removeIf(t -> config.components().stream().anyMatch(c -> c.getId().equals(t.getId()))); + componentDiff.removeIf(t -> config.components().stream().anyMatch(c -> { + return c.getId().equals(t.getId()) // + && c.getFactoryId().equals(t.getFactoryId()); + })); } this.components2Delete.addAll(componentDiff); } @@ -94,8 +97,24 @@ public void create(User user, List otherAppConfigurations) thr if (foundComponentWithSameId != null) { // check if the found component has the same factory id if (!foundComponentWithSameId.getFactoryId().equals(comp.getFactoryId())) { - errors.add("Configuration of component with id '" + foundComponentWithSameId.getId() - + "' can not be rewritten. Because the component has a different factoryId."); + if (this.components2Delete.stream().anyMatch(t -> t.getId().equals(comp.getId()))) { + // if the component was intended to be deleted anyway delete it directly and + // create the new component directly afterwards + try { + this.deleteComponent(user, comp); + this.deletedComponents.add(comp.getId()); + this.components2Delete.removeIf(t -> t.getId().equals(comp.getId())); + this.createComponent(user, comp); + this.createdComponents.add(comp); + } catch (OpenemsNamedException e) { + final var error = "Component[" + comp.getFactoryId() + "] cant be created!"; + errors.add(error); + errors.add(e.getMessage()); + } + } else { + errors.add("Configuration of component with id '" + foundComponentWithSameId.getId() + + "' can not be rewritten. Because the component has a different factoryId."); + } continue; } @@ -179,8 +198,7 @@ public void delete(User user, List otherAppConfigurations) thr } try { - this.componentManager.handleDeleteComponentConfigRequest(user, - new DeleteComponentConfigRequest(comp.getId())); + this.deleteComponent(user, comp); this.deletedComponents.add(comp.getId()); } catch (OpenemsNamedException e) { errors.add(e.toString()); @@ -235,6 +253,10 @@ private final boolean anyChanges() { || !this.components2Delete.isEmpty(); } + private void deleteComponent(User user, EdgeConfig.Component comp) throws OpenemsNamedException { + this.componentManager.handleDeleteComponentConfigRequest(user, new DeleteComponentConfigRequest(comp.getId())); + } + private void createComponent(User user, EdgeConfig.Component comp) throws OpenemsNamedException { List properties = comp.getProperties().entrySet().stream() .map(t -> new Property(t.getKey(), t.getValue())).collect(Collectors.toList()); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties index b0e860ca53e..809b90e995c 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties @@ -123,6 +123,8 @@ App.Evcs.controller.alias = Ladestation Steuerung App.Evcs.ip.description = Die IP-Adresse der Ladestation. App.Evcs.chargingStation.label = Ladepunkt {0} App.Evcs.numberOfChargingStations.label = Anzahl Ladepunkte +App.Evcs.phaseRotation.label = Phasenrotation +App.Evcs.phaseRotation.description = Verkabelung der einzelnen Phasen der Ladestation zu den Phasen im Netz App.Evcs.Cluster.Name = Multiladepunkt Management App.Evcs.Cluster.Name.short = Multiladepunkt Management @@ -246,8 +248,9 @@ App.IntegratedSystem.shadowManagementDisabled.label = Schattenmanagement deaktiv App.IntegratedSystem.shadowManagementDisabled.description = Nur wenn Optimierer verbaut sind, muss das Schattenmanagement deaktiviert werden App.IntegratedSystem.hasEssLimiter14a.label = Hat Limitierer für §14a -App.IntegratedSystem.modbus0.alias = Kommunikation mit der Batterie -App.IntegratedSystem.modbus0N.alias = Kommunikation mit den Batterien +App.IntegratedSystem.modbusToBattery.alias = Kommunikation mit der Batterie +App.IntegratedSystem.modbusToBatteryN.alias = Kommunikation mit den Batterien +App.IntegratedSystem.modbusToBattery0.alias = Kommunikation mit der Batterie {0} App.IntegratedSystem.modbus1.alias = Kommunikation mit dem Batterie-Wechselrichter App.IntegratedSystem.modbus1N.alias = Kommunikation mit dem Batterie-Wechselrichter {0} App.IntegratedSystem.modbus2.alias = externe RS485 Schnittstelle @@ -260,6 +263,7 @@ App.IntegratedSystem.batteryParallelClusterN.alias = Parallel-Cluster {0} App.IntegratedSystem.batteryInverter0.alias = Batterie-Wechselrichter App.IntegratedSystem.batteryInverterN.alias = Batterie-Wechselrichter {0} App.IntegratedSystem.ess0.alias = Speichersystem +App.IntegratedSystem.essCluster0.alias = Batteriespeicher-Cluster App.IntegratedSystem.predictor0.alias = Prognose App.IntegratedSystem.ctrlEssSurplusFeedToGrid0.alias = Überschusseinspeisung App.IntegratedSystem.emergencyMeter.alias = Notstromverbraucher @@ -325,7 +329,6 @@ App.FENECON.Industrial.L.ILK710.batteryFirmwareVersion.label = Battery Firmware App.FENECON.Industrial.S.io0 = Relais App.FENECON.Industrial.S.ess0.alias = Batteriespeicher App.FENECON.Industrial.S.essN.alias = Batteriespeicher {0} -App.FENECON.Industrial.S.essCluster0.alias = Batteriespeicher-Cluster App.FENECON.Industrial.S.hasGridMeter.label = Hat Netzzähler App.FENECON.Industrial.S.hasSelfConsumptionOptimization.label = Hat Eigenverbrauchsoptimierung App.FENECON.Industrial.S.modbusToGridMeter.alias = Kommunikation mit den Netzzähler @@ -448,6 +451,12 @@ App.TimeOfUseTariff.Stromdao.Name = Dynamischer Stromtarif (Stromdao Corrently) App.TimeOfUseTariff.Stromdao.Name.short = Stromdao Corrently App.TimeOfUseTariff.Stromdao.zipCode.label = PLZ App.TimeOfUseTariff.Stromdao.zipCode.description = Deutsche Postleitzahl des Wohnorts +App.TimeOfUseTariff.Swisspower.Name = Dynamischer Stromtarif (Swisspower) +App.TimeOfUseTariff.Swisspower.Name.short = Swisspower +App.TimeOfUseTariff.Swisspower.accessToken.label = Token +App.TimeOfUseTariff.Swisspower.accessToken.description = Bitte stellen Sie den von Swisspower bereitgestellte Zugangstoken bereit. Um ein Token zu erhalten, wenden Sie sich bitte an Swisspower. +App.TimeOfUseTariff.Swisspower.meteringCode.label = Messpunktnummer (z.B. CH1018601234500000000000000011642) +App.TimeOfUseTariff.Swisspower.meteringCode.description = Bitte stellen Sie die von Swisspower bereitgestellte Messpunktnummer bereit. Um ein Messpunktnummer zu erhalten, wenden Sie sich bitte an Swisspower. App.TimeOfUseTariff.Tibber.Name = Dynamischer Stromtarif (Tibber) App.TimeOfUseTariff.Tibber.Name.short = Tibber App.TimeOfUseTariff.Tibber.accessToken.label = Token diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties index 460116c4f56..3614d8cb9f6 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties @@ -123,6 +123,8 @@ App.Evcs.controller.alias = Charging station control App.Evcs.ip.description = The IP address of the charging station. App.Evcs.chargingStation.label = Charging point {0} App.Evcs.numberOfChargingStations.label = Number of charging points +App.Evcs.phaseRotation.label = Phase rotation +App.Evcs.phaseRotation.description = Wiring of the individual phases of the charging station to actual phases of the grid App.Evcs.Cluster.Name = Multi-charging point management App.Evcs.Cluster.Name.short = Multi-charging point management @@ -246,8 +248,9 @@ App.IntegratedSystem.shadowManagementDisabled.label = Deactivate shadow manageme App.IntegratedSystem.shadowManagementDisabled.description = Only if optimisers are installed, shadow management must be deactivated App.IntegratedSystem.hasEssLimiter14a.label = Has limiter for §14a -App.IntegratedSystem.modbus0.alias = Communication with the battery -App.IntegratedSystem.modbus0N.alias = Communication with the batteries +App.IntegratedSystem.modbusToBattery.alias = Communication with the battery +App.IntegratedSystem.modbusToBatteryN.alias = Communication with the batteries +App.IntegratedSystem.modbusToBattery0.alias = Communication with the battery {0} App.IntegratedSystem.modbus1.alias = Communication with the battery inverter App.IntegratedSystem.modbus1N.alias = Communication with the battery inverter {0} App.IntegratedSystem.modbus2.alias = external RS485 interface @@ -260,6 +263,7 @@ App.IntegratedSystem.batteryParallelClusterN.alias = parallel-cluster {0} App.IntegratedSystem.batteryInverter0.alias = battery inverter App.IntegratedSystem.batteryInverterN.alias = battery inverter {0} App.IntegratedSystem.ess0.alias = Storage system +App.IntegratedSystem.essCluster0.alias = Storage system-Cluster App.IntegratedSystem.predictor0.alias = Forecast App.IntegratedSystem.ctrlEssSurplusFeedToGrid0.alias = Excess feed-in App.IntegratedSystem.emergencyMeter.alias = Emergency power consumers @@ -325,7 +329,6 @@ App.FENECON.Industrial.L.ILK710.batteryFirmwareVersion.label = Battery Firmware App.FENECON.Industrial.S.io0 = Relay App.FENECON.Industrial.S.ess0.alias = Battery Storage App.FENECON.Industrial.S.essN.alias = Battery Storage {0} -App.FENECON.Industrial.S.essCluster0.alias = Battery Storage-Cluster App.FENECON.Industrial.S.hasGridMeter.label = Has Grid-Meter App.FENECON.Industrial.S.hasSelfConsumptionOptimization.label = Has Self-consumption optimisation App.FENECON.Industrial.S.modbusToGridMeter.alias = Communication with the Grid-Meter @@ -448,6 +451,12 @@ App.TimeOfUseTariff.Stromdao.Name = Time-of-Use Tariff (STROMDAO Corrently) App.TimeOfUseTariff.Stromdao.Name.short = STROMDAO Corrently App.TimeOfUseTariff.Stromdao.zipCode.label = ZIP Code App.TimeOfUseTariff.Stromdao.zipCode.description = German postal code of place of residence +App.TimeOfUseTariff.Swisspower.Name = Time-of-Use Tariff (Swisspower) +App.TimeOfUseTariff.Swisspower.Name.short = Swisspower +App.TimeOfUseTariff.Swisspower.accessToken.label = Token +App.TimeOfUseTariff.Swisspower.accessToken.description = Please provide personal access token provided by Swisspower. To get one, please contact Swisspower. +App.TimeOfUseTariff.Swisspower.meteringCode.label = Measuring point number (e.g. CH1018601234500000000000000011642) +App.TimeOfUseTariff.Swisspower.meteringCode.description = Please provide Measuring point number received by Swisspower. To get one, please contact Swisspower. App.TimeOfUseTariff.Tibber.Name = Time-of-Use Tariff (Tibber) App.TimeOfUseTariff.Tibber.Name.short = Tibber App.TimeOfUseTariff.Tibber.accessToken.label = Token diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkables.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkables.java index 32783117bed..993cc7d84d3 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkables.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkables.java @@ -1,6 +1,7 @@ package io.openems.edge.core.appmanager.validator; import java.util.Collections; +import java.util.Objects; import java.util.TreeMap; import io.openems.edge.core.appmanager.validator.ValidatorConfig.CheckableConfig; @@ -35,14 +36,25 @@ public static CheckableConfig checkCommercial92() { * * @param check1 the first check * @param check2 the second check + * @param other the additional checks to combine with 'or' operator * @return the {@link CheckableConfig} */ - public static CheckableConfig checkOr(CheckableConfig check1, CheckableConfig check2) { - return new ValidatorConfig.CheckableConfig(CheckOr.COMPONENT_NAME, + public static CheckableConfig checkOr(// + CheckableConfig check1, // + CheckableConfig check2, // + CheckableConfig... other // + ) { + var config = new ValidatorConfig.CheckableConfig(CheckOr.COMPONENT_NAME, new ValidatorConfig.MapBuilder<>(new TreeMap()) // - .put("check1", check1) // - .put("check2", check2) // + .put("check1", Objects.requireNonNull(check1)) // + .put("check2", Objects.requireNonNull(check2)) // .build()); + if (other != null && other.length > 0) { + for (var check : other) { + config = config.or(check); + } + } + return config; } /** diff --git a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/EdgeConfigWorker.java b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/EdgeConfigWorker.java index 9679cfb5a6a..6c90e120dab 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/EdgeConfigWorker.java +++ b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/EdgeConfigWorker.java @@ -595,7 +595,7 @@ private static TreeMap convertProperties(String componentId for (EdgeConfig.Factory.Property property : factory.getProperties()) { var key = property.getId(); - if (EdgeConfig.ignorePropertyKey(key) || EdgeConfig.ignoreComponentPropertyKey(componentId, key)) { + if (EdgeConfig.ignorePropertyKey(key)) { // Ignore this Property continue; } diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/HostImpl.java b/io.openems.edge.core/src/io/openems/edge/core/host/HostImpl.java index bc18b13fab4..b1fa2ec2900 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/host/HostImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/host/HostImpl.java @@ -16,6 +16,7 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.metatype.annotations.Designate; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; @@ -50,6 +51,8 @@ }) public class HostImpl extends AbstractOpenemsComponent implements Host, OpenemsComponent, ComponentJsonApi { + private final Logger log = LoggerFactory.getLogger(HostImpl.class); + protected final OperatingSystem operatingSystem; private final DiskSpaceWorker diskSpaceWorker; @@ -93,6 +96,13 @@ public HostImpl() { e1.printStackTrace(); } } + + this.operatingSystem.getOperatingSystemVersion().whenComplete((name, error) -> { + this._setOsVersion(name); + if (error != null) { + this.log.info("Error while trying to get operating system version", error); + } + }); } @Activate diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystem.java b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystem.java index ef8c3cacbb3..5a39d0b9510 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystem.java +++ b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystem.java @@ -61,4 +61,11 @@ public CompletableFuture handleExecuteSystemComman public CompletableFuture handleExecuteSystemRestartRequest( ExecuteSystemRestartRequest request) throws NotImplementedException; + /** + * Gets the current operating system version. + * + * @return a future with the result + */ + public CompletableFuture getOperatingSystemVersion(); + } 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 f74bd442b16..49e3e99764c 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 @@ -29,6 +29,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import java.util.regex.Pattern; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -203,7 +204,7 @@ private List toFileFormat(User user, NetworkInterface iface) throws O + user.getName()); result.add("# changedAt: " // + LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).toString()); - + // Match Section result.add(MATCH_SECTION); result.add("Name=" + iface.getName()); @@ -220,12 +221,12 @@ private List toFileFormat(User user, NetworkInterface iface) throws O if (iface.getLinkLocalAddressing().isSetAndNotNull()) { result.add("LinkLocalAddressing=" + (iface.getLinkLocalAddressing().getValue() ? "yes" : "no")); } - + var metric = DEFAULT_METRIC; if (iface.getMetric().isSetAndNotNull()) { metric = iface.getMetric().getValue().intValue(); } - + if (iface.getDhcp().isSetAndNotNull()) { var dhcp = iface.getDhcp().getValue(); result.add(EMPTY_SECTION); @@ -577,4 +578,35 @@ protected static NetworkInterface parseSystemdNetworkdConfigurationFile(L dhcp.get(), linkLocalAddressing.get(), gateway.get(), dns.get(), addresses.get(), metric.get(), attachment); } + + @Override + public CompletableFuture getOperatingSystemVersion() { + final var sc = new SystemCommand(// + "cat /etc/os-release", // + false, // runInBackground + 5, // timeoutSeconds + Optional.empty(), // username + Optional.empty()); // password + + final var versionFuture = new CompletableFuture(); + this.execute(sc, success -> { + final var osVersionName = Stream.of(success.stdout()) // + .map(t -> t.split("=", 2)) // + .filter(t -> t.length == 2) // + .filter(t -> t[0].equals("PRETTY_NAME")) // + .map(t -> t[1]) // + .map(t -> { + if (t.startsWith("\"") && t.endsWith("\"")) { + return t.substring(1, t.length() - 1); + } + return t; + }) // + .findAny(); + + osVersionName.ifPresentOrElse(versionFuture::complete, () -> versionFuture + .completeExceptionally(new OpenemsException("OS-Version name not found in /etc/os-release"))); + }, versionFuture::completeExceptionally); + return versionFuture; + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemWindows.java b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemWindows.java index 68c509a86a4..f41c76e7ad5 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemWindows.java +++ b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemWindows.java @@ -50,4 +50,9 @@ public CompletableFuture handleExecuteSystemRe throw new NotImplementedException("ExecuteSystemRestartRequest is not implemented for Windows"); } + @Override + public CompletableFuture getOperatingSystemVersion() { + return CompletableFuture.completedFuture(System.getProperty("os.name")); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/meta/Config.java b/io.openems.edge.core/src/io/openems/edge/core/meta/Config.java index 41b9dd0f734..7e42538a6c0 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/meta/Config.java +++ b/io.openems.edge.core/src/io/openems/edge/core/meta/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.common.currency.CurrencyConfig; +import io.openems.common.types.CurrencyConfig; @ObjectClassDefinition(// name = "Core Meta", // diff --git a/io.openems.edge.core/src/io/openems/edge/core/meta/MetaImpl.java b/io.openems.edge.core/src/io/openems/edge/core/meta/MetaImpl.java index 8ae2c8457f9..0d581be7eab 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/meta/MetaImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/meta/MetaImpl.java @@ -1,5 +1,12 @@ package io.openems.edge.core.meta; +import static io.openems.common.utils.ThreadPoolUtils.shutdownAndAwaitTermination; + +import java.time.Instant; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -12,8 +19,10 @@ import io.openems.common.OpenemsConstants; import io.openems.common.channel.AccessMode; import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.edge.common.channel.LongReadChannel; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.currency.Currency; import io.openems.edge.common.meta.Meta; import io.openems.edge.common.modbusslave.ModbusSlave; import io.openems.edge.common.modbusslave.ModbusSlaveTable; @@ -27,6 +36,8 @@ }) public class MetaImpl extends AbstractOpenemsComponent implements Meta, OpenemsComponent, ModbusSlave { + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + @Reference private ConfigurationAdmin cm; @@ -45,6 +56,12 @@ public MetaImpl() { private void activate(ComponentContext context, Config config) { super.activate(context, SINGLETON_COMPONENT_ID, Meta.SINGLETON_SERVICE_PID, true); + // Update the Channel _meta/SystemTimeUtc after every second + final var systemTimeUtcChannel = this.channel(Meta.ChannelId.SYSTEM_TIME_UTC); + this.executor.scheduleAtFixedRate(() -> { + systemTimeUtcChannel.setNextValue(Instant.now().getEpochSecond()); + }, 0, 1000, TimeUnit.MILLISECONDS); + this.applyConfig(config); if (OpenemsComponent.validateSingleton(this.cm, Meta.SINGLETON_SERVICE_PID, SINGLETON_COMPONENT_ID)) { return; @@ -64,6 +81,7 @@ private void modified(ComponentContext context, Config config) { @Override @Deactivate protected void deactivate() { + shutdownAndAwaitTermination(this.executor, 0); super.deactivate(); } @@ -73,6 +91,6 @@ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { } private void applyConfig(Config config) { - this._setCurrency(config.currency().toCurrency()); + this._setCurrency(Currency.fromCurrencyConfig(config.currency())); } } 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 index 934c788a24f..a674023dd02 100644 --- 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 @@ -166,6 +166,7 @@ private Prediction getPredictionSum(Sum.ChannelId channelId) { switch (meter.getMeterType()) { case GRID: case CONSUMPTION_METERED: + case MANAGED_CONSUMPTION_METERED: case CONSUMPTION_NOT_METERED: return false; case PRODUCTION: 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 6bf8c875d46..4910b1f965e 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 @@ -39,7 +39,6 @@ import io.openems.edge.ess.api.MetaEss; import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.dccharger.api.EssDcCharger; -import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.MetaEvcs; import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.api.VirtualMeter; @@ -267,6 +266,14 @@ private void calculateChannelValues() { // Consumption is positive break; + case MANAGED_CONSUMPTION_METERED: // + if (meter instanceof MetaEvcs) { + // ignore this Evcs + } else { + managedConsumptionActivePower.addValue(meter.getActivePowerChannel()); + } + break; + case CONSUMPTION_NOT_METERED: // TODO CONSUMPTION_NOT_METERED // Consumption is positive @@ -306,17 +313,6 @@ private void calculateChannelValues() { productionDcActualPower.addValue(charger.getActualPowerChannel()); productionDcActiveEnergy.addValue(charger.getActualEnergyChannel()); - } else if (component instanceof Evcs evcs) { - /* - * Electric Vehicle Charging Station - */ - if (evcs instanceof MetaEvcs) { - // ignore this Evcs - continue; - } - - managedConsumptionActivePower.addValue(evcs.getChargePowerChannel()); - } else if (component instanceof TimeOfUseTariff tou) { /* * Time-of-Use-Tariff @@ -464,7 +460,6 @@ private void calculateState() { highestLevel = Level.INFO; } } - this.getStateChannel().setNextValue(highestLevel); } diff --git a/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome30DefaultRelays.java b/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome30DefaultRelays.java index 699921aa573..c67423251bc 100644 --- a/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome30DefaultRelays.java +++ b/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome30DefaultRelays.java @@ -18,7 +18,7 @@ import io.openems.edge.core.appmanager.Apps; import io.openems.edge.core.appmanager.DummyPseudoComponentManager; import io.openems.edge.core.appmanager.OpenemsAppInstance; -import io.openems.edge.io.test.DummyInputOutput; +import io.openems.edge.io.test.DummyCustomInputOutput; public class TestFeneconHome30DefaultRelays { @@ -96,10 +96,10 @@ private final OpenemsAppInstance createFullHomeWithDummyIo() throws Exception { final var instance = TestFeneconHome30.createFullHome30(this.appManagerTestBundle, DUMMY_ADMIN); this.appManagerTestBundle.componentManger.handleDeleteComponentConfigRequest(DUMMY_ADMIN, new DeleteComponentConfigRequest("io0")); - final var dummyRelay = new DummyInputOutput("io0", "RELAY", 1, 8); + final var dummyRelay = new DummyCustomInputOutput("io0", "RELAY", 1, 8); this.appManagerTestBundle.cm.getOrCreateEmptyConfiguration(dummyRelay.id()); ((DummyPseudoComponentManager) this.appManagerTestBundle.componentManger).addComponent(dummyRelay); - final var dummyRelay1 = new DummyInputOutput("io1", "RELAY", 1, 8); + final var dummyRelay1 = new DummyCustomInputOutput("io1", "RELAY", 1, 8); this.appManagerTestBundle.cm.getOrCreateEmptyConfiguration(dummyRelay1.id()); ((DummyPseudoComponentManager) this.appManagerTestBundle.componentManger).addComponent(dummyRelay1); return instance; diff --git a/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHomeDefaultRelays.java b/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHomeDefaultRelays.java index 983785d6092..b8cf0b2bd19 100644 --- a/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHomeDefaultRelays.java +++ b/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHomeDefaultRelays.java @@ -18,7 +18,7 @@ import io.openems.edge.core.appmanager.Apps; import io.openems.edge.core.appmanager.DummyPseudoComponentManager; import io.openems.edge.core.appmanager.OpenemsAppInstance; -import io.openems.edge.io.test.DummyInputOutput; +import io.openems.edge.io.test.DummyCustomInputOutput; public class TestFeneconHomeDefaultRelays { @@ -92,10 +92,10 @@ private final OpenemsAppInstance createFullHomeWithDummyIo() throws Exception { final var instance = TestFeneconHome.createFullHome(this.appManagerTestBundle, DUMMY_ADMIN); this.appManagerTestBundle.componentManger.handleDeleteComponentConfigRequest(DUMMY_ADMIN, new DeleteComponentConfigRequest("io0")); - final var dummyRelay = new DummyInputOutput("io0", "RELAY", 1, 4); + final var dummyRelay = new DummyCustomInputOutput("io0", "RELAY", 1, 4); this.appManagerTestBundle.cm.getOrCreateEmptyConfiguration(dummyRelay.id()); ((DummyPseudoComponentManager) this.appManagerTestBundle.componentManger).addComponent(dummyRelay); - final var dummyRelay1 = new DummyInputOutput("io1", "RELAY", 1, 4); + final var dummyRelay1 = new DummyCustomInputOutput("io1", "RELAY", 1, 4); this.appManagerTestBundle.cm.getOrCreateEmptyConfiguration(dummyRelay1.id()); ((DummyPseudoComponentManager) this.appManagerTestBundle.componentManger).addComponent(dummyRelay1); return instance; diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java index 1252dc6b574..2ec9fd378ee 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java @@ -1,5 +1,7 @@ package io.openems.edge.core.appmanager; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; +import static io.openems.common.utils.ReflectionUtils.setStaticAttributeViaReflection; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -33,7 +35,6 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; -import io.openems.common.utils.ReflectionUtils; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.host.Host; import io.openems.edge.common.test.ComponentTest; @@ -183,8 +184,6 @@ public AppManagerTestBundle(// this.appManagerUtil = new AppManagerUtilImpl(this.componentManger); this.appCenterBackendUtil = new DummyAppCenterBackendUtil(); - ReflectionUtils.setAttribute(this.appManagerUtil.getClass(), this.appManagerUtil, "appManager", this.sut); - this.addCheckable(TestCheckable.COMPONENT_NAME, t -> new TestCheckable()); this.addCheckable(CheckOr.COMPONENT_NAME, t -> new CheckOr(t, this.checkableFactory)); this.checkCardinality = this.addCheckable(CheckCardinality.COMPONENT_NAME, @@ -198,20 +197,13 @@ public AppManagerTestBundle(// this.appValidateWorker = new AppValidateWorker(); final var appConfigValidator = new AppConfigValidator(); - ReflectionUtils.setAttribute(AppValidateWorker.class, this.appValidateWorker, "appManagerUtil", - this.appManagerUtil); - ReflectionUtils.setAttribute(AppValidateWorker.class, this.appValidateWorker, "validator", appConfigValidator); - - ReflectionUtils.setAttribute(AppConfigValidator.class, appConfigValidator, "appManagerUtil", - this.appManagerUtil); - ReflectionUtils.setAttribute(AppConfigValidator.class, appConfigValidator, "tasks", this.appHelper.getTasks()); + setAttributeViaReflection(this.appValidateWorker, "appManagerUtil", this.appManagerUtil); + setAttributeViaReflection(this.appValidateWorker, "validator", appConfigValidator); - // use this so the appManagerAppHelper does not has to be a OpenemsComponent and - // the attribute can still be private - ReflectionUtils.setAttribute(this.appHelper.getClass(), this.appHelper, "appManager", this.sut); - ReflectionUtils.setAttribute(this.appHelper.getClass(), this.appHelper, "appManagerUtil", this.appManagerUtil); + setAttributeViaReflection(appConfigValidator, "appManagerUtil", this.appManagerUtil); + setAttributeViaReflection(appConfigValidator, "tasks", this.appHelper.getTasks()); - ReflectionUtils.setAttribute(DependencyUtil.class, null, "appHelper", this.appHelper); + setStaticAttributeViaReflection(DependencyUtil.class, "appHelper", this.appHelper); new ComponentTest(this.sut) // .addReference("cm", this.cm) // diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java index 4fa625f043d..a5892cd16c8 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java @@ -67,6 +67,7 @@ import io.openems.edge.app.timeofusetariff.RabotCharge; import io.openems.edge.app.timeofusetariff.StadtwerkHassfurt; import io.openems.edge.app.timeofusetariff.StromdaoCorrently; +import io.openems.edge.app.timeofusetariff.Swisspower; import io.openems.edge.app.timeofusetariff.Tibber; import io.openems.edge.common.component.ComponentManager; @@ -175,6 +176,16 @@ public static final StadtwerkHassfurt stadtwerkHassfurt(AppManagerTestBundle t) return app(t, StadtwerkHassfurt::new, "App.TimeOfUseTariff.Hassfurt"); } + /** + * Test method for creating a {@link Swisspower}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final Swisspower swisspower(AppManagerTestBundle t) { + return app(t, Swisspower::new, "App.TimeOfUseTariff.Swisspower"); + } + /** * Test method for creating a {@link RabotCharge}. * diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java index 9fe35111886..9ad1f56bc01 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java @@ -1,11 +1,12 @@ package io.openems.edge.core.appmanager; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; + import java.util.ArrayList; import java.util.List; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.dependency.AppManagerAppHelper; @@ -24,12 +25,12 @@ public DummyAppManagerAppHelper(// ComponentManager componentManager, // ComponentUtil componentUtil, // AppManagerUtil util // - ) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + ) throws ReflectionException { this.tasks = new ArrayList>(); this.impl = new AppManagerAppHelperImpl(componentManager, componentUtil); - ReflectionUtils.setAttribute(AppManagerAppHelperImpl.class, this.impl, "tasks", this.tasks); - ReflectionUtils.setAttribute(AppManagerAppHelperImpl.class, this.impl, "appManagerUtil", util); + setAttributeViaReflection(this.impl, "tasks", this.tasks); + setAttributeViaReflection(this.impl, "appManagerUtil", util); } /** diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java index c14160b59af..252f68b5d30 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java @@ -46,6 +46,9 @@ public void beforeEach() throws Exception { .build())); this.apps.add(new TestTranslation(Apps.stadtwerkHassfurt(t), true, JsonUtils.buildJsonObject() // .build())); + this.apps.add(new TestTranslation(Apps.swisspower(t), true, JsonUtils.buildJsonObject() // + .addProperty("METERING_CODE", "bf7777") // + .build())); this.apps.add(new TestTranslation(Apps.stromdaoCorrently(t), true, JsonUtils.buildJsonObject() // .addProperty("ZIP_CODE", "123456789") // .build())); diff --git a/io.openems.edge.core/test/io/openems/edge/core/meta/MetaImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/meta/MetaImplTest.java index 946dafc96a0..3c6a00657d5 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/meta/MetaImplTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/meta/MetaImplTest.java @@ -1,6 +1,6 @@ package io.openems.edge.core.meta; -import static io.openems.edge.common.currency.CurrencyConfig.EUR; +import static io.openems.common.types.CurrencyConfig.EUR; import org.junit.Test; diff --git a/io.openems.edge.core/test/io/openems/edge/core/meta/MyConfig.java b/io.openems.edge.core/test/io/openems/edge/core/meta/MyConfig.java index b99b018b74a..c269dc60237 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/meta/MyConfig.java +++ b/io.openems.edge.core/test/io/openems/edge/core/meta/MyConfig.java @@ -1,7 +1,7 @@ package io.openems.edge.core.meta; import io.openems.common.test.AbstractComponentConfig; -import io.openems.edge.common.currency.CurrencyConfig; +import io.openems.common.types.CurrencyConfig; import io.openems.edge.common.meta.Meta; @SuppressWarnings("all") diff --git a/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java index 934e934489e..3bc99e30cdc 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java @@ -1,19 +1,17 @@ package io.openems.edge.core.predictormanager; +import static io.openems.edge.common.test.TestUtils.createDummyClock; import static io.openems.edge.predictor.api.prediction.Prediction.EMPTY_PREDICTION; import static java.time.temporal.ChronoUnit.DAYS; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import java.time.Instant; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; import org.junit.Test; import io.openems.common.exceptions.OpenemsException; -import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.ComponentTest; @@ -60,7 +58,7 @@ public class PredictorManagerImplTest { @Test public void test() throws OpenemsException, Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T00:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); final var cm = new DummyComponentManager(clock); final var sum = new DummySum(); final var midnight = ZonedDateTime.now(clock).truncatedTo(DAYS); diff --git a/io.openems.edge.core/test/io/openems/edge/core/sum/ExtremeEverValuesTest.java b/io.openems.edge.core/test/io/openems/edge/core/sum/ExtremeEverValuesTest.java index 86641c06232..4befb39052f 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/sum/ExtremeEverValuesTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/sum/ExtremeEverValuesTest.java @@ -1,21 +1,20 @@ package io.openems.edge.core.sum; import static io.openems.edge.common.test.TestUtils.activateNextProcessImage; +import static io.openems.edge.common.test.TestUtils.createDummyClock; import static io.openems.edge.common.test.TestUtils.withValue; import static io.openems.edge.core.sum.ExtremeEverValues.Range.NEGATIVE; import static io.openems.edge.core.sum.ExtremeEverValues.Range.POSTIVE; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import java.io.IOException; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; import org.junit.Test; import io.openems.common.exceptions.OpenemsException; -import io.openems.common.test.TimeLeapClock; import io.openems.edge.common.sum.Sum; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; @@ -25,12 +24,12 @@ public class ExtremeEverValuesTest { @Test public void test() throws OpenemsException, Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T20:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); var cm = new DummyConfigurationAdmin(); var sum = new SumImpl(); new ComponentTest(sum) // .addReference("cm", cm) // - .addReference("componentManager", new DummyComponentManager()) // + .addReference("componentManager", new DummyComponentManager(clock)) // .activate(MyConfig.create() // .setGridMinActivePower(0) // .setIgnoreStateComponents() // @@ -63,14 +62,14 @@ public void test() throws OpenemsException, Exception { assertEquals(0, getProperty(cm, "gridMinActivePower")); // Still the same - clock.leap(24, ChronoUnit.HOURS); + clock.leap(24, HOURS); withValue(sum, Sum.ChannelId.GRID_ACTIVE_POWER, -100); sut.update(sum, cm); activateNextProcessImage(sum); assertEquals(0, getProperty(cm, "gridMinActivePower")); // 24 hours passed -> update config - clock.leap(1, ChronoUnit.SECONDS); + clock.leap(1, SECONDS); withValue(sum, Sum.ChannelId.GRID_ACTIVE_POWER, -101); sut.update(sum, cm); activateNextProcessImage(sum); @@ -78,7 +77,7 @@ public void test() throws OpenemsException, Exception { assertEquals(-101, getProperty(cm, "gridMinActivePower")); // Update Channel; not the config - clock.leap(1, ChronoUnit.SECONDS); + clock.leap(1, SECONDS); withValue(sum, Sum.ChannelId.GRID_ACTIVE_POWER, -101); sut.update(sum, cm); activateNextProcessImage(sum); diff --git a/io.openems.edge.core/test/io/openems/edge/core/sum/SumImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/sum/SumImplTest.java index 77a25ef888b..2ef7670b28d 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/sum/SumImplTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/sum/SumImplTest.java @@ -1,39 +1,46 @@ package io.openems.edge.core.sum; -import static io.openems.edge.meter.api.MeterType.GRID; +import static io.openems.common.types.MeterType.GRID; +import static io.openems.edge.common.sum.Sum.ChannelId.CONSUMPTION_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.CONSUMPTION_MAX_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.ESS_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.ESS_DISCHARGE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_MAX_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_MIN_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.PRODUCTION_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.PRODUCTION_MAX_ACTIVE_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.UNMANAGED_CONSUMPTION_ACTIVE_POWER; import org.junit.Test; import io.openems.common.exceptions.OpenemsException; -import io.openems.common.types.ChannelAddress; +import io.openems.common.types.MeterType; +import io.openems.edge.common.filter.DisabledRampFilter; 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.meter.api.MeterType; +import io.openems.edge.evcs.test.DummyEvcsPower; +import io.openems.edge.evcs.test.DummyManagedEvcs; import io.openems.edge.meter.test.DummyElectricityMeter; public class SumImplTest { - private static final ChannelAddress GRID_MIN_ACTIVE_POWER = new ChannelAddress("_sum", "GridMinActivePower"); - private static final ChannelAddress GRID_MAX_ACTIVE_POWER = new ChannelAddress("_sum", "GridMaxActivePower"); - private static final ChannelAddress PRODUCTION_MAX_ACTIVE_POWER = new ChannelAddress("_sum", - "ProductionMaxActivePower"); - private static final ChannelAddress CONSUMPTION_MAX_ACTIVE_POWER = new ChannelAddress("_sum", - "ConsumptionMaxActivePower"); - @Test public void test() throws OpenemsException, Exception { - var sut = new SumImpl(); - var cm = new DummyConfigurationAdmin(); - var grid = new DummyElectricityMeter("meter0") // + final var sut = new SumImpl(); + final var grid = new DummyElectricityMeter("meter0") // .withMeterType(GRID); // - var pv = new DummyElectricityMeter("meter1") // + final var pv = new DummyElectricityMeter("meter1") // .withMeterType(MeterType.PRODUCTION); // - var test = new ComponentTest(sut) // + final var evcs = new DummyManagedEvcs("evcs0", new DummyEvcsPower(new DisabledRampFilter())) // + .withMeterType(MeterType.MANAGED_CONSUMPTION_METERED); + final var test = new ComponentTest(sut) // .addComponent(grid) // .addComponent(pv) // - .addReference("cm", cm) // + .addComponent(evcs) // + .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // .setGridMinActivePower(0) // @@ -42,8 +49,16 @@ public void test() throws OpenemsException, Exception { grid.withActivePower(-1000); pv.withActivePower(5555); + evcs.withActivePower(1000); test.next(new TestCase() // .onBeforeProcessImage(() -> sut.updateChannelsBeforeProcessImage()) // + .output(GRID_ACTIVE_POWER, -1000) // + .output(PRODUCTION_ACTIVE_POWER, 5555) // + .output(ESS_ACTIVE_POWER, null) // + .output(ESS_DISCHARGE_POWER, null) // + .output(CONSUMPTION_ACTIVE_POWER, 4555) // + .output(UNMANAGED_CONSUMPTION_ACTIVE_POWER, 3555) // + .output(GRID_MIN_ACTIVE_POWER, -1000) // .output(GRID_MAX_ACTIVE_POWER, 0) // .output(PRODUCTION_MAX_ACTIVE_POWER, 5555) // @@ -53,6 +68,13 @@ public void test() throws OpenemsException, Exception { pv.withActivePower(6666); test.next(new TestCase() // .onBeforeProcessImage(() -> sut.updateChannelsBeforeProcessImage()) // + .output(GRID_ACTIVE_POWER, -2000) // + .output(PRODUCTION_ACTIVE_POWER, 6666) // + .output(ESS_ACTIVE_POWER, null) // + .output(ESS_DISCHARGE_POWER, null) // + .output(CONSUMPTION_ACTIVE_POWER, 4666) // + .output(UNMANAGED_CONSUMPTION_ACTIVE_POWER, 3666) // + .output(GRID_MIN_ACTIVE_POWER, -2000) // .output(GRID_MAX_ACTIVE_POWER, 0) // .output(PRODUCTION_MAX_ACTIVE_POWER, 6666) // @@ -61,6 +83,13 @@ public void test() throws OpenemsException, Exception { grid.withActivePower(3000); test.next(new TestCase() // .onBeforeProcessImage(() -> sut.updateChannelsBeforeProcessImage()) // + .output(GRID_ACTIVE_POWER, 3000) // + .output(PRODUCTION_ACTIVE_POWER, 6666) // + .output(ESS_ACTIVE_POWER, null) // + .output(ESS_DISCHARGE_POWER, null) // + .output(CONSUMPTION_ACTIVE_POWER, 9666) // + .output(UNMANAGED_CONSUMPTION_ACTIVE_POWER, 8666) // + .output(GRID_MIN_ACTIVE_POWER, -2000) // .output(GRID_MAX_ACTIVE_POWER, 3000) // .output(PRODUCTION_MAX_ACTIVE_POWER, 6666) // diff --git a/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/common/AbstractEdge2Edge.java b/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/common/AbstractEdge2Edge.java index 9dae53bdbeb..581576f2275 100644 --- a/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/common/AbstractEdge2Edge.java +++ b/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/common/AbstractEdge2Edge.java @@ -344,20 +344,14 @@ protected abstract io.openems.edge.common.channel.ChannelId getWriteChannelId( * @return the {@link AbstractModbusElement} */ private static ModbusElement generateModbusElement(ModbusType type, int address) { - switch (type) { - case ENUM16: - case UINT16: - return new UnsignedWordElement(address); - case UINT32: - return new UnsignedDoublewordElement(address); - case FLOAT32: - return new FloatDoublewordElement(address); - case FLOAT64: - return new UnsignedQuadruplewordElement(address); - case STRING16: - return new StringWordElement(address, 16); - } - return null; + return switch (type) { + case ENUM16, UINT16 -> new UnsignedWordElement(address); + case UINT32 -> new UnsignedDoublewordElement(address); + case UINT64 -> new UnsignedQuadruplewordElement(address); + case FLOAT32 -> new FloatDoublewordElement(address); + case FLOAT64 -> new UnsignedQuadruplewordElement(address); + case STRING16 -> new StringWordElement(address, 16); + }; } /** diff --git a/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/meter/Config.java b/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/meter/Config.java index 2622f6c7e6d..81e8c94500b 100644 --- a/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/meter/Config.java +++ b/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/meter/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Edge-2-Edge Meter", // diff --git a/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/meter/Edge2EdgeMeterImpl.java b/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/meter/Edge2EdgeMeterImpl.java index 7890cb4b9a5..5f9fd7402ea 100644 --- a/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/meter/Edge2EdgeMeterImpl.java +++ b/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/meter/Edge2EdgeMeterImpl.java @@ -17,6 +17,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; import io.openems.edge.common.component.OpenemsComponent; @@ -25,7 +26,6 @@ import io.openems.edge.edge2edge.common.AbstractEdge2Edge; import io.openems.edge.edge2edge.common.Edge2Edge; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/ess/Edge2EdgeEssImplTest.java b/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/ess/Edge2EdgeEssImplTest.java index 90317ad9e80..6b200696166 100644 --- a/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/ess/Edge2EdgeEssImplTest.java +++ b/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/ess/Edge2EdgeEssImplTest.java @@ -10,19 +10,16 @@ public class Edge2EdgeEssImplTest { - private static final String COMPONENT_ID = "ess0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new Edge2EdgeEssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setModbusId(MODBUS_ID) // + .setId("ess0") // + .setModbusId("modbus0") // .setRemoteAccessMode(AccessMode.READ_WRITE) // - .setRemoteComponentId(COMPONENT_ID) // + .setRemoteComponentId("ess0") // .build()) .next(new TestCase()); } diff --git a/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/meter/Edge2EdgeEssMeterImplTest.java b/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/meter/Edge2EdgeEssMeterImplTest.java index a52aed0a22b..ab0e6bd99ac 100644 --- a/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/meter/Edge2EdgeEssMeterImplTest.java +++ b/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/meter/Edge2EdgeEssMeterImplTest.java @@ -1,28 +1,26 @@ package io.openems.edge.edge2edge.meter; +import static io.openems.common.types.MeterType.PRODUCTION; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class Edge2EdgeEssMeterImplTest { - private static final String COMPONENT_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new Edge2EdgeMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setModbusId(MODBUS_ID) // - .setRemoteComponentId(COMPONENT_ID) // - .setMeterType(MeterType.PRODUCTION) // + .setId("meter0") // + .setModbusId("modbus0") // + .setRemoteComponentId("meter0") // + .setMeterType(PRODUCTION) // .build()) .next(new TestCase()); } diff --git a/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/meter/MyConfig.java b/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/meter/MyConfig.java index a78c1af3e8a..5a10b708e13 100644 --- a/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/meter/MyConfig.java +++ b/io.openems.edge.edge2edge/test/io/openems/edge/edge2edge/meter/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.edge2edge.meter; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.energy.api/bnd.bnd b/io.openems.edge.energy.api/bnd.bnd index 49a30e8f94b..e7874ed1370 100644 --- a/io.openems.edge.energy.api/bnd.bnd +++ b/io.openems.edge.energy.api/bnd.bnd @@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.controller.api,\ io.openems.edge.predictor.api,\ io.openems.edge.timeofusetariff.api,\ + org.apache.commons.math3,\ -testpath: \ ${testpath},\ diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyConstants.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyConstants.java new file mode 100644 index 00000000000..0cca2cc68df --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyConstants.java @@ -0,0 +1,15 @@ +package io.openems.edge.energy.api; + +import io.openems.common.types.ChannelAddress; + +public class EnergyConstants { + + public static final int PERIODS_PER_HOUR = 4; + + public static final ChannelAddress SUM_PRODUCTION = new ChannelAddress("_sum", "ProductionActivePower"); + public static final ChannelAddress SUM_UNMANAGED_CONSUMPTION = new ChannelAddress("_sum", + "UnmanagedConsumptionActivePower"); + + private EnergyConstants() { + } +} diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergySchedulable.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergySchedulable.java index cf2f7e66e0d..601a80179c8 100644 --- a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergySchedulable.java +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergySchedulable.java @@ -2,12 +2,12 @@ import io.openems.edge.controller.api.Controller; -public interface EnergySchedulable extends Controller { +public interface EnergySchedulable extends Controller { /** * Get the {@link EnergyScheduleHandler}. * * @return {@link EnergyScheduleHandler} */ - public EnergyScheduleHandler getEnergyScheduleHandler(); + public EnergyScheduleHandler getEnergyScheduleHandler(); } diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyScheduleHandler.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyScheduleHandler.java index ca5ca2ebc45..bad193c7dd6 100644 --- a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyScheduleHandler.java +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyScheduleHandler.java @@ -1,76 +1,477 @@ package io.openems.edge.energy.api; +import static com.google.common.base.MoreObjects.toStringHelper; import static io.openems.common.utils.DateUtils.roundDownToQuarter; +import java.time.Clock; import java.time.ZonedDateTime; -import java.util.Optional; +import java.util.Arrays; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.function.IntFunction; import java.util.function.Supplier; +import java.util.stream.IntStream; -import com.google.common.collect.ImmutableMap; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableSortedMap; -public class EnergyScheduleHandler { +import io.openems.edge.controller.api.Controller; +import io.openems.edge.energy.api.EnergyScheduleHandler.WithDifferentStates.PostProcessor; +import io.openems.edge.energy.api.simulation.EnergyFlow; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; +import io.openems.edge.energy.api.simulation.OneSimulationContext; - private final Supplier availableStates; - private final Supplier context; - - private ImmutableMap> schedule = ImmutableMap.of(); - - public EnergyScheduleHandler(Supplier availableStates, Supplier context) { - this.availableStates = availableStates; - this.context = context; - } +public sealed interface EnergyScheduleHandler { /** - * Gets the available States. + * Creates an {@link EnergyScheduleHandler} for a {@link Controller} with + * different states that can be evaluated. * - * @return an Array of States + * @param the type of the State + * @param the type of the Context + * @param defaultState the default State if no other is explicitly scheduled + * @param statesSupplier a {@link Supplier} for available States + * @param contextFunction a {@link Function} to create a Context + * @param simulator a simulator that modifies a given {@link EnergyFlow} + * @return an {@link EnergyScheduleHandler} */ - public STATE[] getAvailableStates() { - return this.availableStates.get(); + public static EnergyScheduleHandler.WithDifferentStates of(// + STATE defaultState, // + Supplier statesSupplier, // + Function contextFunction, // + WithDifferentStates.Simulator simulator) { + return new EnergyScheduleHandler.WithDifferentStates(defaultState, statesSupplier, + contextFunction, simulator, EnergyScheduleHandler.WithDifferentStates.PostProcessor.doNothing()); } /** - * Gets the Context. + * Creates an {@link EnergyScheduleHandler} for a {@link Controller} with + * different states that can be evaluated. * - * @return the Context + * @param the type of the State + * @param the type of the Context + * @param defaultState the default State if no other is explicitly scheduled + * @param statesSupplier a {@link Supplier} for available States + * @param contextFunction a {@link Function} to create a Context + * @param simulator a simulator that modifies a given {@link EnergyFlow} + * @param postProcessor a {@link PostProcessor} + * @return an {@link EnergyScheduleHandler} */ - public CONTEXT getContext() { - return this.context.get(); - } - - public static record Period(STATE state, Integer essChargeInChargeGrid) { + public static EnergyScheduleHandler.WithDifferentStates of(// + STATE defaultState, // + Supplier statesSupplier, // + Function contextFunction, // + WithDifferentStates.Simulator simulator, // + WithDifferentStates.PostProcessor postProcessor) { + return new EnergyScheduleHandler.WithDifferentStates(defaultState, statesSupplier, + contextFunction, simulator, postProcessor); } /** - * Sets the Schedule. Called by Optimizer. + * Creates an {@link EnergyScheduleHandler} for a {@link Controller} with only a + * single state. * - * @param schedule the Schedule + * @param the type of the Context + * @param contextFunction a {@link Function} to create a Context + * @param simulator a simulator that modifies a given {@link EnergyFlow} + * @return an {@link EnergyScheduleHandler} */ - public synchronized void setSchedule(ImmutableMap> schedule) { - this.schedule = schedule; + public static EnergyScheduleHandler.WithOnlyOneState of(// + Function contextFunction, // + WithOnlyOneState.Simulator simulator) { + return new EnergyScheduleHandler.WithOnlyOneState(contextFunction, simulator); } /** - * Gets the current State or null. - * - * @return the State or null + * Triggers Rescheduling by the Energy Scheduler. */ - public synchronized STATE getCurrentState() { - return Optional.ofNullable(this.schedule.get(roundDownToQuarter(ZonedDateTime.now()))) // - .map(Period::state) // - .orElse(null); + public void triggerReschedule(); + + public abstract static sealed class AbstractEnergyScheduleHandler implements EnergyScheduleHandler { + + private final Function contextFunction; + + protected Clock clock; + protected CONTEXT context; + private Runnable onRescheduleCallback; + + public AbstractEnergyScheduleHandler(Function contextFunction) { + this.contextFunction = contextFunction; + } + + /** + * Initialize the {@link EnergyScheduleHandler}. + * + *

+ * This method is called internally before a Simulation is executed. + * + * @param asc the {@link GlobalSimulationsContext} + */ + public void initialize(GlobalSimulationsContext asc) { + this.clock = asc.clock(); + this.context = this.contextFunction.apply(asc); + } + + /** + * This method sets the callback for events that require Rescheduling. + * + * @param callback the {@link Runnable} callback + */ + public synchronized void setOnRescheduleCallback(Runnable callback) { + this.onRescheduleCallback = callback; + } + + /** + * This method removes the callback. + */ + public synchronized void removeOnRescheduleCallback() { + this.onRescheduleCallback = null; + } + + @Override + public void triggerReschedule() { + var onRescheduleCallback = this.onRescheduleCallback; + if (onRescheduleCallback != null) { + onRescheduleCallback.run(); + } + } + + protected ZonedDateTime getNow() { + var clock = this.clock; + if (clock != null) { + return ZonedDateTime.now(clock); + } + return ZonedDateTime.now(); + } + + protected void buildToString(MoreObjects.ToStringHelper toStringHelper) { + var context = this.context; + if (context != null) { + toStringHelper.addValue(context); + } + } } - // TODO hacky... find a better way! - /** - * Gets the current essChargeInChargeGrid or null. - * - * @return the essChargeInChargeGrid or null - */ - public synchronized Integer getCurrentEssChargeInChargeGrid() { - return Optional.ofNullable(this.schedule.get(roundDownToQuarter(ZonedDateTime.now()))) // - .map(Period::essChargeInChargeGrid) // - .orElse(null); + public static final class WithDifferentStates extends AbstractEnergyScheduleHandler { + + public static interface Simulator { + /** + * Simulates a Period. + * + * @param osc the {@link OneSimulationContext} + * @param period the {@link GlobalSimulationsContext.Period} + * @param model the {@link EnergyFlow.Model} + * @param context the Controller Context + * @param state the simulated State + */ + public void simulate(OneSimulationContext osc, GlobalSimulationsContext.Period period, + EnergyFlow.Model model, CONTEXT context, STATE state); + } + + public static interface PostProcessor { + + /** + * A 'do-nothing' {@link PostProcessor}. + * + * @param the type of the State + * @return the same State + */ + public static PostProcessor doNothing() { + return (energyFlow, state) -> state; + } + + /** + * Post-Process a state of a Period during Simulation, i.e. replace with + * 'better' state with the equivalent behaviour. + * + *

+ * NOTE: heavy computation is ok here, because this method is called only at the + * end with the best Schedule. + * + * @param energyFlow the {@link EnergyFlow} + * @param state the initial state + * @return the new state + */ + public STATE postProcess(EnergyFlow energyFlow, STATE state); + } + + private final STATE defaultState; + private final Supplier availableStatesSupplier; + private final Simulator simulator; + private final WithDifferentStates.PostProcessor postProcessor; + private final SortedMap> schedule = new TreeMap<>(); + + private STATE[] availableStates; + + private WithDifferentStates(// + STATE defaultState, // + Supplier availableStatesSupplier, // + Function contextFunction, // + Simulator simulator, // + WithDifferentStates.PostProcessor postProcessor) { + super(contextFunction); + this.defaultState = defaultState; + this.availableStatesSupplier = availableStatesSupplier; + this.simulator = simulator; + this.postProcessor = postProcessor; + } + + @Override + public void initialize(GlobalSimulationsContext asc) { + super.initialize(asc); + this.availableStates = this.availableStatesSupplier.get(); + } + + /** + * Gets the default State. + * + * @return the default State + */ + public STATE getDefaultState() { + return this.defaultState; + } + + /** + * Gets the index of the default State. + * + * @return the index of the default State + */ + public int getDefaultStateIndex() { + var states = this.availableStates; + if (states == null) { + throw new IllegalAccessError( + "EnergySchedulerHandler is uninitialized. `initialize()` must be called first."); + } + return IntStream.range(0, states.length) // + .filter(i -> states[i] == this.defaultState) // + .findFirst() // + .orElse(0 /* fallback */); + } + + /** + * Gets the available States. + * + * @return an Array of States + */ + public STATE[] getAvailableStates() { + return this.availableStates; + } + + /** + * Simulates a Period. + * + * @param osc the {@link OneSimulationContext} + * @param period the simulated {@link GlobalSimulationsContext.Period} + * @param model the {@link EnergyFlow.Model} + * @param stateIndex the index of the simulated state + */ + public void simulatePeriod(OneSimulationContext osc, GlobalSimulationsContext.Period period, + EnergyFlow.Model model, int stateIndex) { + this.simulator.simulate(osc, period, model, this.context, this.availableStates[stateIndex]); + } + + /** + * Post-processes a Period of the best Schedule. + * + *

+ * This method is called internally after the Simulations are executed with the + * found best Schedule. + * + * @param period the {@link GlobalSimulationsContext.Period} + * @param osc the {@link OneSimulationContext} + * @param energyFlow the {@link EnergyFlow} + * @param stateIndex the index of the simulated state + * @return the post-processed state index + */ + public int postProcessPeriod(GlobalSimulationsContext.Period period, OneSimulationContext osc, + EnergyFlow energyFlow, int stateIndex) { + return this.getStateIndex(this.postProcessor.postProcess(energyFlow, this.availableStates[stateIndex])); + } + + public static record Period( + /** STATE of the Period */ + STATE state, + /** Price [1/MWh] */ + double price, // + /** EnergyScheduleHandler Context */ + CONTEXT context, // + /** Simulated EnergyFlow */ + EnergyFlow energyFlow, // + /** the initial ESS energy in the beginning of the period in [Wh] */ + int essInitialEnergy) { + + /** + * This class is only used internally to apply the Schedule. + */ + public static record Transition(int stateIndex, double price, EnergyFlow energyFlow, int essInitialEnergy) { + } + + /** + * Builds a {@link EnergyScheduleHandler.WithDifferentStates.Period} from a + * {@link EnergyScheduleHandler.WithDifferentStates.Period.Transition} record. + * + * @param the type of the State + * @param the type of the Context + * @param t the + * {@link EnergyScheduleHandler.WithDifferentStates.Period.Transition} + * record + * @param getState a method to translate a 'stateIndex' to a STATE + * @param context the CONTEXT used during simulation + * @return a {@link Period} record + */ + public static Period fromTransitionRecord(Period.Transition t, + IntFunction getState, CONTEXT context) { + return new Period<>(getState.apply(t.stateIndex), t.price, context, t.energyFlow, t.essInitialEnergy); + } + } + + /** + * Applies a new Schedule. + * + *

+ * This method is called by the {@link EnergyScheduler}. + * + * @param schedule the new Schedule as Map of ZonedDateTime to State-Index + */ + public void applySchedule(ImmutableSortedMap schedule) { + final var thisQuarter = roundDownToQuarter(this.getNow()); + final var nextQuarter = thisQuarter.plusMinutes(15); + final var currentContext = this.context; + synchronized (this.schedule) { + // Clear outdated entries + this.schedule.headMap(thisQuarter).clear(); + + // Remove future entries + this.schedule.tailMap(nextQuarter).clear(); + + // Update entries from param + var states = this.availableStates; + if (states.length == 0) { + System.err.println("States is empty!"); // TODO proper log + return; + } + schedule.forEach((k, t) -> { + this.schedule.put(k, Period.fromTransitionRecord(t, this::getState, currentContext)); + }); + } + } + + /** + * Gets a copy of the current Schedule. + * + * @return the Schedule + */ + public ImmutableSortedMap> getSchedule() { + synchronized (this.schedule) { + return ImmutableSortedMap.copyOfSorted(this.schedule); + } + } + + /** + * Gets the current {@link Period} record. + * + * @return the record of the currently scheduled Period; possibly null + */ + public Period getCurrentPeriod() { + synchronized (this.schedule) { + final var thisQuarter = roundDownToQuarter(this.getNow()); + return this.schedule.get(thisQuarter); + } + } + + /** + * Gets the string representation for the given stateIndex. + * + * @param stateIndex the index of the state + * @return string representation + */ + public String toStateString(int stateIndex) { + return this.getState(stateIndex).toString(); + } + + /** + * Gets the STATE for the given stateIndex. + * + * @param stateIndex the stateIndex + * @return the STATE + */ + private STATE getState(int stateIndex) { + var states = this.availableStates; + return stateIndex < states.length // + ? states[stateIndex] // + : this.defaultState; + } + + /** + * Gets the stateIndex for the given STATE. + * + * @param state the STATE + * @return the stateIndex; or zero if not found + */ + private int getStateIndex(STATE state) { + var states = this.availableStates; + for (var i = 0; i < states.length; i++) { + if (states[i] == state) { + return i; + } + } + return 0; + } + + @Override + public String toString() { + var toStringHelper = toStringHelper("ESH.WithDifferentStates"); + var availableStates = this.availableStates; + if (availableStates != null) { + toStringHelper.add("availableStates", Arrays.toString(availableStates)); + } + super.buildToString(toStringHelper); + return toStringHelper.toString(); + } } + public static final class WithOnlyOneState extends AbstractEnergyScheduleHandler { + + public static interface Simulator { + /** + * Simulates a Period. + * + * @param osc the {@link OneSimulationContext} + * @param period the {@link GlobalSimulationsContext.Period} + * @param model the {@link EnergyFlow.Model} + * @param context the Controller Context + */ + public void simulate(OneSimulationContext osc, GlobalSimulationsContext.Period period, + EnergyFlow.Model model, CONTEXT context); + } + + private final Simulator simulator; + + private WithOnlyOneState(// + Function contextFunction, // + Simulator simulator) { + super(contextFunction); + this.simulator = simulator; + } + + /** + * Simulates a Period. + * + * @param simContext the {@link OneSimulationContext} + * @param period the {@link GlobalSimulationsContext.Period} + * @param model the {@link EnergyFlow.Model} + */ + public void simulatePeriod(OneSimulationContext simContext, GlobalSimulationsContext.Period period, + EnergyFlow.Model model) { + this.simulator.simulate(simContext, period, model, this.context); + } + + @Override + public String toString() { + var toStringHelper = toStringHelper("ESH.WithOnlyOneState"); + super.buildToString(toStringHelper); + return toStringHelper.toString(); + } + } } diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyScheduler.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyScheduler.java index 7d58264b26b..f48c0344a78 100644 --- a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyScheduler.java +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyScheduler.java @@ -1,19 +1,22 @@ package io.openems.edge.energy.api; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponse; +import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.OpenemsComponent; -import io.openems.edge.common.jsonapi.ComponentJsonApi; +import io.openems.edge.common.jsonapi.Call; /** * The global Energy Schedule optimizer singleton. */ -public interface EnergyScheduler extends OpenemsComponent, ComponentJsonApi { +public interface EnergyScheduler extends OpenemsComponent { public static final String SINGLETON_SERVICE_PID = "Core.Energy"; public static final String SINGLETON_COMPONENT_ID = "_energy"; public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - ; + SIMULATIONS_PER_QUARTER(Doc.of(OpenemsType.INTEGER)); private final Doc doc; @@ -26,4 +29,14 @@ public Doc doc() { return this.doc; } } + + /** + * Handles a GetScheduleRequest. + * + * @param call the JsonApi {@link Call} + * @param id the Component-ID of the Controller + * @return the GetScheduleResponse + */ + @Deprecated + public JsonrpcResponse handleGetScheduleRequestV1(Call call, String id); } diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyUtils.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyUtils.java new file mode 100644 index 00000000000..e499135eade --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/EnergyUtils.java @@ -0,0 +1,125 @@ +package io.openems.edge.energy.api; + +import static io.openems.edge.common.type.TypeUtils.multiply; +import static io.openems.edge.common.type.TypeUtils.orElse; +import static io.openems.edge.energy.api.EnergyConstants.PERIODS_PER_HOUR; +import static java.util.Arrays.stream; + +import java.util.Objects; +import java.util.stream.IntStream; + +public class EnergyUtils { + + private EnergyUtils() { + } + + /** + * Converts a State-of-Charge [%] to Energy [Wh]. + * + * @param totalEnergy the total energy in [Wh] + * @param soc the State-of-Charge in [%] + * @return the energy in [Wh] + */ + public static int socToEnergy(int totalEnergy, int soc) { + return totalEnergy /* [Wh] */ / 100 * soc; + } + + /** + * Finds the first valley in an array of doubles, e.g. prices. + * + * @param fromIndex start searching from this index + * @param values the values array + * @return the index of the valley + */ + public static int findFirstValleyIndex(int fromIndex, double[] values) { + if (values.length <= fromIndex) { + return fromIndex; + } else { + var previous = values[fromIndex]; + for (var i = fromIndex + 1; i < values.length; i++) { + var value = values[i]; + if (value > previous) { + return i - 1; + } + previous = value; + } + } + return values.length - 1; + } + + /** + * Finds the first peak in an array of doubles, e.g. prices. + * + * @param fromIndex start searching from this index + * @param values the values array + * @return the index of the peak + */ + public static int findFirstPeakIndex(int fromIndex, double[] values) { + if (values.length <= fromIndex) { + return fromIndex; + } else { + var previous = values[fromIndex]; + for (var i = fromIndex + 1; i < values.length; i++) { + var value = values[i]; + if (value < previous) { + return i - 1; + } + previous = value; + } + } + return values.length - 1; + } + + /** + * Converts power [W] to energy [Wh/15 min]. + * + * @param power the power value + * @return the energy value + */ + public static int toEnergy(int power) { + return power / PERIODS_PER_HOUR; + } + + /** + * Converts energy [Wh/15 min] to power [W]. + * + * @param energy the energy value + * @return the power value + */ + public static Integer toPower(Integer energy) { + return multiply(energy, PERIODS_PER_HOUR); + } + + /** + * Interpolate an Array of {@link Integer}s. + * + *

+ * Replaces nulls with previous value. If first entry is null, it is set to + * first available value. If all values are null, all are set to 0. + * + * @param values the values + * @return values without nulls + */ + public static int[] interpolateArray(Integer[] values) { + var firstNonNull = stream(values) // + .filter(Objects::nonNull) // + .findFirst(); + var lastNonNullIndex = IntStream.range(0, values.length) // + .filter(i -> values[i] != null) // + .reduce((first, second) -> second); // + if (lastNonNullIndex.isEmpty()) { + return new int[0]; + } + var result = new int[lastNonNullIndex.getAsInt() + 1]; + if (firstNonNull.isEmpty()) { + // all null + return result; + } + int last = firstNonNull.get(); + for (var i = 0; i < result.length; i++) { + int value = orElse(values[i], last); + result[i] = last = value; + } + return result; + } +} diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/Version.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/Version.java new file mode 100644 index 00000000000..41012cf10de --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/Version.java @@ -0,0 +1,20 @@ +package io.openems.edge.energy.api; + +public enum Version { + /** + * Version 1. + * + *

+ * Well tested and production ready, but applies only to + * "Controller.Ess.Time-Of-Use-Tariff". + */ + V1_ESS_ONLY, // + /** + * Version 1. + * + *

+ * Work-in-progress that uses new EnergySchedulable interface to provide real + * multi-objective optimization. + */ + V2_ENERGY_SCHEDULABLE +} diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/Coefficient.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/Coefficient.java new file mode 100644 index 00000000000..14fa453d462 --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/Coefficient.java @@ -0,0 +1,36 @@ +package io.openems.edge.energy.api.simulation; + +import static com.google.common.base.CaseFormat.UPPER_CAMEL; +import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE; + +public enum Coefficient { + /* _sum/Production; positive */ + PROD, + /* _sum/Consumption; positive */ + CONS, + /* _sum/EssActivePower; charge negative; discharge positive */ + ESS, + /* _sum/EssActivePower; sell-to-grid negative, buy-from-grid positive */ + GRID, + /* Production -> Consumption, positive */ + PROD_TO_CONS, + /* Production -> Grid, positive */ + PROD_TO_GRID, + /* Production -> ESS, positive */ + PROD_TO_ESS, + /* Grid -> Consumption, positive */ + GRID_TO_CONS, + /* ESS -> Consumption, positive */ + ESS_TO_CONS, + /* Grid -> ESS, discharge-to-grid negative, charge-from-grid positive */ + GRID_TO_ESS; + + /** + * Gets the {@link Coefficient#name()} in CamelCase. + * + * @return name + */ + public String toCamelCase() { + return UPPER_UNDERSCORE.to(UPPER_CAMEL, this.name()); + } +} \ No newline at end of file diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/EnergyFlow.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/EnergyFlow.java new file mode 100644 index 00000000000..4984c83a8ae --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/EnergyFlow.java @@ -0,0 +1,657 @@ +package io.openems.edge.energy.api.simulation; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static io.openems.edge.energy.api.simulation.Coefficient.CONS; +import static io.openems.edge.energy.api.simulation.Coefficient.ESS; +import static io.openems.edge.energy.api.simulation.Coefficient.ESS_TO_CONS; +import static io.openems.edge.energy.api.simulation.Coefficient.GRID; +import static io.openems.edge.energy.api.simulation.Coefficient.GRID_TO_CONS; +import static io.openems.edge.energy.api.simulation.Coefficient.GRID_TO_ESS; +import static io.openems.edge.energy.api.simulation.Coefficient.PROD; +import static io.openems.edge.energy.api.simulation.Coefficient.PROD_TO_CONS; +import static io.openems.edge.energy.api.simulation.Coefficient.PROD_TO_ESS; +import static io.openems.edge.energy.api.simulation.Coefficient.PROD_TO_GRID; +import static java.lang.Double.NaN; +import static java.lang.Math.min; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; +import static org.apache.commons.math3.optim.linear.Relationship.EQ; +import static org.apache.commons.math3.optim.linear.Relationship.GEQ; +import static org.apache.commons.math3.optim.linear.Relationship.LEQ; +import static org.apache.commons.math3.optim.nonlinear.scalar.GoalType.MAXIMIZE; +import static org.apache.commons.math3.optim.nonlinear.scalar.GoalType.MINIMIZE; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +import org.apache.commons.math3.exception.MathIllegalStateException; +import org.apache.commons.math3.optim.PointValuePair; +import org.apache.commons.math3.optim.linear.LinearConstraint; +import org.apache.commons.math3.optim.linear.LinearConstraintSet; +import org.apache.commons.math3.optim.linear.LinearObjectiveFunction; +import org.apache.commons.math3.optim.linear.Relationship; +import org.apache.commons.math3.optim.linear.SimplexSolver; +import org.apache.commons.math3.optim.nonlinear.scalar.GoalType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext.Period; + +/** + * Holds the {@link Solution} of an {@link EnergyFlow.Model} and provides helper + * functions to access the individual {@link Coefficient}s. + */ +public class EnergyFlow { + + private static final Logger LOG = LoggerFactory.getLogger(EnergyFlow.class); + + private final double[] point; + + private EnergyFlow(PointValuePair pvp) { + this.point = pvp.getPointRef(); + } + + /** + * Gets {@link Coefficient#PROD}. + * + * @return the value + */ + public int getProd() { + return this.getValue(PROD); + } + + /** + * Gets {@link Coefficient#CONS}. + * + * @return the value + */ + public int getCons() { + return this.getValue(CONS); + } + + /** + * Gets {@link Coefficient#ESS}. + * + * @return the value + */ + public int getEss() { + return this.getValue(ESS); + } + + /** + * Gets {@link Coefficient#GRID}. + * + * @return the value + */ + public int getGrid() { + return this.getValue(GRID); + } + + /** + * Gets {@link Coefficient#PROD_TO_CONS}. + * + * @return the value + */ + public int getProdToCons() { + return this.getValue(PROD_TO_CONS); + } + + /** + * Gets {@link Coefficient#PROD_TO_ESS}. + * + * @return the value + */ + public int getProdToEss() { + return this.getValue(PROD_TO_ESS); + } + + /** + * Gets {@link Coefficient#PROD_TO_GRID}. + * + * @return the value + */ + public int getProdToGrid() { + return this.getValue(PROD_TO_GRID); + } + + /** + * Gets {@link Coefficient#GRID_TO_CONS}. + * + * @return the value + */ + public int getGridToCons() { + return this.getValue(GRID_TO_CONS); + } + + /** + * Gets {@link Coefficient#GRID_TO_ESS}. + * + * @return the value + */ + public int getGridToEss() { + return this.getValue(GRID_TO_ESS); + } + + /** + * Gets {@link Coefficient#ESS_TO_CONS}. + * + * @return the value + */ + public int getEssToCons() { + return this.getValue(ESS_TO_CONS); + } + + private int getValue(Coefficient coefficient) { + return toInt(this.point[coefficient.ordinal()]); + } + + /** + * Prints all {@link Coefficient}s and their values line by line. + */ + public void print() { + for (var c : Coefficient.values()) { + LOG.info(c.toCamelCase() + ": " + this.getValue(c)); + } + } + + @Override + public String toString() { + return toStringHelper(this) // + .addValue(stream(Coefficient.values()) // + .map(c -> new StringBuilder() // + .append(c.toCamelCase()) // + .append("=") // + .append(this.getValue(c)) // + .toString()) // + .collect(joining(", "))) // + .toString(); + } + + /** + * Models an EnergyFlow as a Linear Equation System with defined + * {@link Coefficient}s for GRID, ESS, CONS, etc. + */ + public static class Model { + + /** + * Generates a {@link EnergyFlow.Model} from a {@link OneSimulationContext} and + * a {@link Period}. + * + * @param osc the {@link OneSimulationContext} + * @param period the {@link Period} + * @return a new {@link EnergyFlow.Model} + */ + public static EnergyFlow.Model from(OneSimulationContext osc, Period period) { + final int factor; // TODO replace with switch in Java 21 + if (period instanceof GlobalSimulationsContext.Period.Hour) { + factor = 4; + } else { + factor = 1; + } + final var ess = osc.global.ess(); + final var grid = osc.global.grid(); + return new EnergyFlow.Model(// + /* production */ period.production(), // + /* consumption */ period.consumption(), // + /* essMaxCharge */ min(ess.maxChargeEnergy() * factor, ess.totalEnergy() - osc.getEssInitial()), // + /* essMaxDischarge */ min(ess.maxDischargeEnergy() * factor, osc.getEssInitial()), // + /* gridMaxBuy */ grid.maxBuy() * factor, // + /* gridMaxSell */ grid.maxSell() * factor); + } + + public final int production; + public final int consumption; + public final int essMaxCharge; + public final int essMaxDischarge; + public final int gridMaxBuy; + public final int gridMaxSell; + + private final List constraints = new ArrayList(); + + public Model(int production, int consumption, int essMaxCharge, int essMaxDischarge, int gridMaxBuy, + int gridMaxSell) { + this.production = production; + this.consumption = consumption; + this.essMaxCharge = essMaxCharge; + this.essMaxDischarge = essMaxDischarge; + this.gridMaxBuy = gridMaxBuy; + this.gridMaxSell = gridMaxSell; + + this + // Internal Relationships + .addConstraint(c -> c // Sum + .setCoefficient(PROD, 1) // + .setCoefficient(ESS, 1) // + .setCoefficient(GRID, 1) // + .setCoefficient(CONS, -1) // + .toLinearConstraint(EQ, 0)) // + .addConstraint(c -> c // Distribute Production + .setCoefficient(PROD, -1) // + .setCoefficient(PROD_TO_CONS, 1) // + .setCoefficient(PROD_TO_ESS, 1) // + .setCoefficient(PROD_TO_GRID, 1) // + .toLinearConstraint(EQ, 0)) // + .addConstraint(b -> b // Distribute Consumption + .setCoefficient(CONS, 1) // + .setCoefficient(ESS_TO_CONS, -1) // + .setCoefficient(GRID_TO_CONS, -1) // + .setCoefficient(PROD_TO_CONS, -1) // + .toLinearConstraint(EQ, 0)) // + .addConstraint(b -> b // Distribute Grid + .setCoefficient(GRID, -1) // + .setCoefficient(PROD_TO_GRID, -1) // + .setCoefficient(GRID_TO_CONS, 1) // + .setCoefficient(GRID_TO_ESS, 1) // + .toLinearConstraint(EQ, 0)) // + .addConstraint(b -> b // Distribute ESS + .setCoefficient(ESS, -1) // + .setCoefficient(PROD_TO_ESS, -1) // + .setCoefficient(ESS_TO_CONS, 1) // + .setCoefficient(GRID_TO_ESS, -1) // + .toLinearConstraint(EQ, 0)) // + .addConstraint(b -> b // Only Positive PROD_TO_ESS + .setCoefficient(PROD_TO_ESS, 1) // + .toLinearConstraint(GEQ, 0)) // + .addConstraint(b -> b // Only Positive PROD_TO_GRID + .setCoefficient(PROD_TO_GRID, 1) // + .toLinearConstraint(GEQ, 0)) // + .addConstraint(b -> b // Only Positive ESS_TO_CONS + .setCoefficient(ESS_TO_CONS, 1) // + .toLinearConstraint(GEQ, 0)) // + .addConstraint(b -> b // Only Positive GRID_TO_CONS + .setCoefficient(GRID_TO_CONS, 1) // + .toLinearConstraint(GEQ, 0)) + + // Production & Consumption + .addConstraint(c -> c // + .setCoefficient(PROD, 1) // + .toLinearConstraint(EQ, production)) // + .addConstraint(c -> c // + .setCoefficient(CONS, 1) // + .toLinearConstraint(EQ, consumption)) + .addConstraint(b -> b // PROD_TO_CONS + .setCoefficient(PROD_TO_CONS, 1) // + .toLinearConstraint(EQ, min(production, consumption))) + + // ESS Max Charge/Discharge + .addConstraint(c -> c // + .setCoefficient(ESS, 1) // + .toLinearConstraint(GEQ, -essMaxCharge)) // + .addConstraint(c -> c // + .setCoefficient(ESS, 1) // + .toLinearConstraint(LEQ, essMaxDischarge)) // + // Grid Max Buy/Sell + .addConstraint(c -> c // + .setCoefficient(GRID, 1) // + .toLinearConstraint(LEQ, gridMaxBuy)) // + .addConstraint(c -> c // + .setCoefficient(GRID, 1) // + .toLinearConstraint(GEQ, -gridMaxSell)); + } + + /** + * Sets the {@link Coefficient#ESS} Charge/Discharge Energy to the given value, + * while making sure the value fits in the active constraints. + * + * @param value the value + * @return actually set value; {@link Double#NaN} on error + */ + public double setEss(int value) { + return this.setFittingCoefficientValue(ESS, EQ, value); + } + + /** + * Limits the {@link Coefficient#ESS} Charge Energy to the given value, while + * making sure the value fits in the active constraints. + * + * @param value the value + * @return actually set value; {@link Double#NaN} on error + */ + public double setEssMaxCharge(int value) { + return this.setFittingCoefficientValue(ESS, GEQ, -value); + } + + /** + * Limits the {@link Coefficient#ESS} Discharge Energy to the given value, while + * making sure the value fits in the active constraints. + * + * @param value the value + * @return actually set value; {@link Double#NaN} on error + */ + public double setEssMaxDischarge(int value) { + return this.setFittingCoefficientValue(ESS, LEQ, value); + } + + /** + * Limits the {@link Coefficient#GRID} Buy Energy to the given value, while + * making sure the value fits in the active constraints. + * + * @param value the value + * @return actually set value; {@link Double#NaN} on error + */ + public double setGridMaxBuy(int value) { + return this.setFittingCoefficientValue(ESS, LEQ, value); + } + + /** + * Limits the {@link Coefficient#GRID} Sell Energy to the given value, while + * making sure the value fits in the active constraints. + * + * @param value the value + * @return actually set value; {@link Double#NaN} on error + */ + public double setGridMaxSell(int value) { + return this.setFittingCoefficientValue(ESS, GEQ, -value); + } + + /** + * Prints a table with all constraints. + */ + public void logConstraints() { + { + var b = new StringBuilder(); + for (var coefficient : Coefficient.values()) { + b.append(String.format("%s ", coefficient.toCamelCase())); + } + LOG.info(b.toString()); + } + for (var constraint : this.constraints) { + var b = new StringBuilder(); + var equation = constraint.getCoefficients(); + for (var coefficient : Coefficient.values()) { + b.append(String.format("% " + coefficient.name().length() + ".0f ", + equation.getEntry(coefficient.ordinal()))); + } + b.append(String.format("%2s % 10.0f", constraint.getRelationship(), constraint.getValue())); + LOG.info(b.toString()); + } + } + + /** + * Prints min/max values for a {@link Coefficient}. + * + * @param coefficient the {@link Coefficient} + */ + public void logMinMaxValues(Coefficient coefficient) { + var values = this.calculateMinMaxValues(coefficient); + var min = values[0]; + var max = values[1]; + LOG.info(String.format("%-12s % 5.0f % 5.0f %s", coefficient.toCamelCase(), min, max, + min == max ? "fixed" : "")); + } + + /** + * Prints a table with all constraints. + */ + public void logMinMaxValues() { + LOG.info(String.format("%-12s %5s %5s", "Coefficient", "Min", "Max")); + for (var coefficient : Coefficient.values()) { + this.logMinMaxValues(coefficient); + } + } + + @Override + public String toString() { + return "EnergyFlow.Model[" // + + Arrays.stream(Coefficient.values()) // + .map(coefficient -> { + var values = this.calculateMinMaxValues(coefficient); + var min = values[0]; + var max = values[1]; + var b = new StringBuilder().append(coefficient.toCamelCase()) // + .append("=") // + .append(min); + if (min == max) { + b // + .append("|fixed"); + } else { + b // + .append("|") // + .append(max); // + } + return b.toString(); + }) // + .collect(joining(",")) // + + "]"; + } + + /** + * Calculates the current Min and Max values for a given {@link Coefficient}. + * + * @param coefficient the {@link Coefficient} + * @return result[0] is the Min value; result[1] is the Max value + */ + private double[] calculateMinMaxValues(Coefficient coefficient) { + final double[] result = new double[2]; + try { + result[0] = this.getExtremeCoefficientValue(coefficient, MINIMIZE); + } catch (MathIllegalStateException e) { + result[0] = NaN; + } + try { + result[1] = this.getExtremeCoefficientValue(coefficient, MAXIMIZE); + } catch (MathIllegalStateException e) { + result[1] = NaN; + } + return result; + } + + private EnergyFlow.Model addConstraint(Function coefficients) { + this.constraints.add(coefficients.apply(new Coefficients())); + return this; + } + + /** + * Gets the minimum or maximum allowed value for the given {@link Coefficient}. + * + * @param coefficient the {@link Coefficient} + * @param goalType the {@link GoalType} + * @return the value + * @throws MathIllegalStateException if this {@link EnergyFlow.Model} is + * unsolvable + */ + public double getExtremeCoefficientValue(Coefficient coefficient, GoalType goalType) + throws MathIllegalStateException { + return solve(goalType, this.constraints, Coefficients.create() // + .setCoefficient(coefficient, 1) // + .toLinearObjectiveFunction(0)) // + .getPointRef()[coefficient.ordinal()]; + } + + /** + * Adds a {@link LinearConstraint} that sets the given {@link Coefficient} to + * the minimum or maximum allowed value. + * + * @param coefficient the {@link Coefficient} + * @param goalType the {@link GoalType} + */ + public void setExtremeCoefficientValue(Coefficient coefficient, GoalType goalType) { + try { + var value = this.getExtremeCoefficientValue(coefficient, goalType); + this.setCoefficientValue(coefficient, value); + } catch (MathIllegalStateException e) { + LOG.warn("[setExtremeCoefficientValue] " // + + "Unable to " + goalType + " " + coefficient + ": " + e.getMessage() + " " // + + this.toString()); + } + } + + /** + * Adds a {@link LinearConstraint} that sets the given {@link Coefficient} to + * the given value, while making sure the value fits in the active constraints. + * + * @param coefficient the {@link Coefficient} + * @param relationship the {@link Relationship}l + * @param value the value + * @return actually set value; {@link Double#NaN} on error + */ + public double setFittingCoefficientValue(Coefficient coefficient, Relationship relationship, double value) { + // Fit to MIN value + try { + var min = this.getExtremeCoefficientValue(coefficient, MINIMIZE); + if (value <= min) { + this.setCoefficientValue(coefficient, relationship, min); + return min; + } + } catch (MathIllegalStateException e) { + LOG.warn("[setFittingCoefficientValue] " // + + "Unable to MINIMIZE " + coefficient + ": " + e.getMessage() + " " // + + this.toString()); + return NaN; + } + + // Fit to MAX value + try { + var max = this.getExtremeCoefficientValue(coefficient, MAXIMIZE); + if (value > max) { + this.setCoefficientValue(coefficient, relationship, max); + return max; + } + } catch (MathIllegalStateException e) { + LOG.warn("[setFittingCoefficientValue] " // + + "Unable to MAXIMIZE " + coefficient + ": " + e.getMessage() + " " // + + this.toString()); + return NaN; + } + + // Apply coefficient value + this.setCoefficientValue(coefficient, relationship, value); + return value; + } + + /** + * Adds a {@link LinearConstraint} that sets the given {@link Coefficient} to + * the given value. + * + * @param coefficient the {@link Coefficient} + * @param value the value + */ + private void setCoefficientValue(Coefficient coefficient, double value) { + this.setCoefficientValue(coefficient, Relationship.EQ, value); + } + + /** + * Adds a {@link LinearConstraint} that constrains the given {@link Coefficient} + * to the given value and {@link Relationship}. + * + * @param coefficient the {@link Coefficient} + * @param relationship the {@link Relationship} + * @param value the value + */ + private void setCoefficientValue(Coefficient coefficient, Relationship relationship, double value) { + this.addConstraint(c -> c // + .setCoefficient(coefficient, 1) // + .toLinearConstraint(relationship, value)); + } + + /** + * Solves the {@link EnergyFlow.Model} and returns an {@link EnergyFlow}. + * + * @return the {@link EnergyFlow}; null if this {@link EnergyFlow.Model} is + * unsolvable + */ + public EnergyFlow solve() { + final double ess; + try { + ess = this.getExtremeCoefficientValue(ESS, MAXIMIZE); + } catch (MathIllegalStateException e) { + LOG.warn("[solve] " // + + "Unable to MAXIMIZE ESS: " + e.getMessage() + " " // + + this.toString()); + return null; + } + if (ess <= 0) { + // ESS Charge or Zero; GRID_TO_ESS must be >= 0 + this.setFittingCoefficientValue(GRID_TO_ESS, GEQ, 0); + this.setFittingCoefficientValue(ESS_TO_CONS, EQ, 0); + } + if (ess >= 0) { + // ESS Discharge or Zero + // Maximize ESS_TO_CONS (1st prio: PROD_TO_CONS; 3rd prio: GRID_TO_CONS) + final double essMax; + try { + essMax = this.getExtremeCoefficientValue(ESS_TO_CONS, MAXIMIZE); + } catch (MathIllegalStateException e) { + LOG.warn("[solve] " // + + "Unable to MAXIMIZE ESS_TO_CONS: " + e.getMessage() + " " // + + this.toString()); + return null; + } + this.setCoefficientValue(ESS_TO_CONS, min(essMax, ess)); + } + + var coefficients = initializeCoefficients(); + Arrays.fill(coefficients, 1); + try { + return new EnergyFlow(solve(MINIMIZE, this.constraints, new LinearObjectiveFunction(coefficients, 0))); + } catch (MathIllegalStateException e) { + LOG.warn("[solve] " // + + "Unable to solve EnergyFlow.Model: " + e.getMessage() + " " // + + this.toString()); + return null; + } + } + + /** + * Solves the linear equation system. + * + * @param goalType {@link GoalType#MINIMIZE} or + * {@link GoalType#MAXIMIZE} the objective function + * @param constraints the {@link LinearConstraint}s + * @param objectiveFunction the {@link LinearObjectiveFunction} + * @return the {@link PointValuePair} + * @throws MathIllegalStateException if this {@link EnergyFlow.Model} is + * unsolvable + */ + private static PointValuePair solve(GoalType goalType, Collection constraints, + LinearObjectiveFunction objectiveFunction) throws MathIllegalStateException { + return new SimplexSolver().optimize(// + objectiveFunction, // + new LinearConstraintSet(constraints), // + goalType); + } + } + + /** + * Helper class to provides a Builder-Pattern like way to create a coefficients + * array suitable for a {@link LinearConstraint} or + * {@link LinearObjectiveFunction}. + */ + private static class Coefficients { + + private static Coefficients create() { + return new Coefficients(); + } + + private final double[] coefficients; + + private Coefficients() { + this.coefficients = initializeCoefficients(); + } + + private Coefficients setCoefficient(Coefficient coefficient, int value) { + this.coefficients[coefficient.ordinal()] = value; + return this; + } + + private LinearConstraint toLinearConstraint(Relationship relationship, double value) { + return new LinearConstraint(this.coefficients, relationship, value); + } + + private LinearObjectiveFunction toLinearObjectiveFunction(int constantTerm) { + return new LinearObjectiveFunction(this.coefficients, constantTerm); + } + + } + + private static double[] initializeCoefficients() { + return new double[Coefficient.values().length]; + } + + private static int toInt(double value) { + return (int) Math.round(value); + } +} diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/GlobalSimulationsContext.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/GlobalSimulationsContext.java new file mode 100644 index 00000000000..e9bd6d9aa3b --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/GlobalSimulationsContext.java @@ -0,0 +1,325 @@ +package io.openems.edge.energy.api.simulation; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static io.openems.edge.common.type.TypeUtils.assertNull; +import static io.openems.edge.energy.api.EnergyConstants.SUM_PRODUCTION; +import static io.openems.edge.energy.api.EnergyConstants.SUM_UNMANAGED_CONSUMPTION; +import static io.openems.edge.energy.api.EnergyUtils.socToEnergy; +import static io.openems.edge.energy.api.EnergyUtils.toEnergy; +import static java.lang.Math.abs; +import static java.lang.Math.min; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.stream.IntStream; + +import com.google.common.collect.ImmutableList; + +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.utils.DateUtils; +import io.openems.edge.common.sum.Sum; +import io.openems.edge.common.type.TypeUtils; +import io.openems.edge.energy.api.EnergySchedulable; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext.Period.Hour; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext.Period.Quarter; +import io.openems.edge.predictor.api.manager.PredictorManager; +import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; + +/** + * Holds the simulation context that is used globally for all simulations in one + * optimization run. + * + *

+ * This record is usually created once per quarter. + */ +public record GlobalSimulationsContext(// + Clock clock, // + /** Start-Timestamp */ + ZonedDateTime startTime, // + ImmutableList handlers, // + Grid grid, // + Ess ess, // + /** + * Period is either mixed, with {@link Hour}s and {@link Quarter}s, or + * {@link Quarter}s only. + */ + ImmutableList periods) { + + @Override + public String toString() { + return toStringHelper(this) // + .add("startTime", this.startTime) // + .addValue(this.grid) // + .addValue(this.ess) // + .add("handlers", this.handlers) // + .toString(); + } + + public static record Ess(// + /** ESS Currently Available Energy (SoC in [Wh]) */ + int currentEnergy, // + /** ESS Total Energy (Capacity) [Wh] */ + int totalEnergy, // + /** ESS Max Charge Energy [Wh] */ + int maxChargeEnergy, // + /** ESS Max Discharge Energy [Wh] */ + int maxDischargeEnergy) { + } + + public static record Grid(// + /** Max Buy-From-Grid Energy [Wh] */ + int maxBuy, // + /** Max Sell-To-Grid Energy [Wh] */ + int maxSell) { + } + + public static sealed interface Period { + /** + * Start-Timestamp of the Period. + * + * @return the {@link ZonedDateTime} + */ + public ZonedDateTime time(); + + /** + * Production prediction for the Period in [Wh]. + * + * @return the production prediction + */ + public int production(); + + /** + * Consumption prediction for the Period in [Wh]. + * + * @return the consumption prediction + */ + public int consumption(); + + /** + * (Average) Grid-Buy-Price for the Period in [1/MWh]. + * + * @return the price + */ + public double price(); + + public static record Quarter(// + ZonedDateTime time, // + int production, // + int consumption, // + double price // + ) implements Period { + } + + public static record Hour(// + ZonedDateTime time, // + int production, // + int consumption, // + double price, // + /** Raw Periods, representing one QUARTER. */ + ImmutableList quarterPeriods // + ) implements Period { + } + } + + public static class Builder { + private Clock clock; + private ImmutableList handlers; + private Sum sum; + private PredictorManager predictorManager; + private TimeOfUseTariff timeOfUseTariff; + + /** + * The {@link Clock}. + * + * @param clock the {@link Clock} + * @return myself + */ + public Builder setClock(Clock clock) { + this.clock = clock; + return this; + } + + /** + * The {@link EnergyScheduleHandler}s of the {@link EnergySchedulable}s. + * + *

+ * The list is sorted by Scheduler. + * + * @param handlers the list of {@link EnergyScheduleHandler}s + * @return myself + */ + public Builder setEnergyScheduleHandlers(ImmutableList handlers) { + this.handlers = handlers; + return this; + } + + /** + * The {@link Sum}. + * + * @param sum the {@link Sum} + * @return myself + */ + public Builder setSum(Sum sum) { + this.sum = sum; + return this; + } + + /** + * The {@link PredictorManager}. + * + * @param predictorManager the {@link PredictorManager} + * @return myself + */ + public Builder setPredictorManager(PredictorManager predictorManager) { + this.predictorManager = predictorManager; + return this; + } + + /** + * The {@link TimeOfUseTariff}. + * + * @param timeOfUseTariff the {@link TimeOfUseTariff} + * @return myself + */ + public Builder setTimeOfUseTariff(TimeOfUseTariff timeOfUseTariff) { + this.timeOfUseTariff = timeOfUseTariff; + return this; + } + + /** + * Builds the {@link GlobalSimulationsContext}. + * + * @return the {@link GlobalSimulationsContext} record + */ + public GlobalSimulationsContext build() throws OpenemsException, IllegalArgumentException { + assertNull("Clock is not available", this.clock); + assertNull("EnergyScheduleHandlers are not available", this.handlers); + assertNull("Sum is not available", this.sum); + assertNull("Predictor-Manager is not available", this.predictorManager); + assertNull("TimeOfUseTariff is not available", this.timeOfUseTariff); + + final var startTime = DateUtils.roundDownToQuarter(ZonedDateTime.now(this.clock)); + + // Prediction values + final var consumptions = this.predictorManager.getPrediction(SUM_UNMANAGED_CONSUMPTION).asArray(); + final var productions = generateProductionPrediction(// + this.predictorManager.getPrediction(SUM_PRODUCTION).asArray(), // + consumptions.length); + + // Prices contains the price values and the time it is retrieved. + final var prices = this.timeOfUseTariff.getPrices().asArray(); + + final var numberOfPeriods = min(min(consumptions.length, productions.length), prices.length); + if (numberOfPeriods == 0) { + throw new IllegalArgumentException("No forecast periods available. " // + + "Consumptions[" + consumptions.length + "] " // + + "Productions[" + productions.length + "] " // + + "Prices[" + prices.length + "]"); + } + + // Helpers + final IntFunction toQuarterPeriod = (i) -> new Period.Quarter(// + startTime.plusMinutes(i * 15), // + toEnergy(productions[i]), // + toEnergy(consumptions[i]), // + prices[i]); + final var periodLengthHourFromIndex = calculatePeriodDurationHourFromIndex(startTime); + + var periods = ImmutableList.builder(); + + // Quarters + for (var i = 0; i < min(periodLengthHourFromIndex, numberOfPeriods); i++) { + periods.add(toQuarterPeriod.apply(i)); + } + + // Hours + final Function range = (i) -> IntStream.range(i, min(i + 4, numberOfPeriods)); + for (int i = periodLengthHourFromIndex, hour = periodLengthHourFromIndex / 4; // + i < numberOfPeriods; // + i += 4, hour++) { + periods.add(new Period.Hour(// + startTime.plusHours(hour), // + toEnergy(range.apply(i).map(j -> productions[j]).sum()), // + toEnergy(range.apply(i).map(j -> consumptions[j]).sum()), // + range.apply(i).mapToDouble(j -> prices[j]).average().getAsDouble(), // + range.apply(i) // + .mapToObj(j -> toQuarterPeriod.apply(j)) // + .collect(toImmutableList()))); + } + + final Ess ess; + { + var essTotalEnergy = this.sum.getEssCapacity().getOrError(); + var essInitialEnergy = socToEnergy(essTotalEnergy, this.sum.getEssSoc().getOrError()); + + // Power Values for scheduling battery for individual periods. + var maxDischargePower = TypeUtils.max(1000 /* at least 1000 W */, // + this.sum.getEssMaxDischargePower().get()); + var maxChargePower = TypeUtils.min(-1000 /* at least 1000 W */, // + this.sum.getEssMinDischargePower().get()); + // TODO + // if (context.limitChargePowerFor14aEnWG()) { + // maxChargePower = max(ESS_LIMIT_14A_ENWG, maxChargePower); // Apply §14a EnWG + // limit + // } + + ess = new Ess(essInitialEnergy, essTotalEnergy, toEnergy(abs(maxChargePower)), + toEnergy(maxDischargePower)); + } + final var grid = new Grid(40000 /* TODO */, 20000 /* TODO */); + + return new GlobalSimulationsContext(this.clock, startTime, this.handlers, grid, ess, periods.build()); + } + } + + /** + * Create a {@link GlobalSimulationsContext} {@link Builder}. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new GlobalSimulationsContext.Builder(); + } + + /** + * Postprocesses production prediction; makes sure length is at least the same + * as consumption prediction - filling up with zeroes. + * + * @param prediction the production prediction + * @param minLength the min length (= consumption prediction length) + * @return new production prediction + */ + protected static Integer[] generateProductionPrediction(Integer[] prediction, int minLength) { + if (prediction.length >= minLength) { + return prediction; + } + return IntStream.range(0, minLength) // + .mapToObj(i -> i > prediction.length - 1 ? 0 : prediction[i]) // + .toArray(Integer[]::new); + } + + /** + * Calculates the index when Period duration switches from {@link Hour} to + * {@link Quarter}. + * + *

+ * The index is calculated as "6 hours" plus remaining quarters of the current + * hour. + * + * @param time Start-Timestamp of the Schedule + * @return the index + */ + // TODO this should be set depending on the actual calculation time and + // quality of the best schedule result + protected static int calculatePeriodDurationHourFromIndex(ZonedDateTime time) { + var minute = time.getMinute(); + if (minute == 0) { + minute = 60; + } + return 6 * 4 + (60 - minute) / 15; + } +} diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/OneSimulationContext.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/OneSimulationContext.java new file mode 100644 index 00000000000..0e6244a2975 --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/OneSimulationContext.java @@ -0,0 +1,59 @@ +package io.openems.edge.energy.api.simulation; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.lang.Math.max; + +/** + * Holds the simulation context that is used for one simulation of a full + * schedule with multiple periods. + * + *

+ * This record is usually created multiple times per second. + */ +public class OneSimulationContext { + + /** + * Builds a {@link OneSimulationContext}. + * + * @param asc the {@link GlobalSimulationsContext} + * @return the {@link OneSimulationContext} + */ + public static OneSimulationContext from(GlobalSimulationsContext asc) { + return new OneSimulationContext(asc, asc.ess().currentEnergy()); + } + + public final GlobalSimulationsContext global; + + private int essInitialEnergy; + + private OneSimulationContext(GlobalSimulationsContext gsc, int essInitialEnergy) { + this.global = gsc; + this.essInitialEnergy = essInitialEnergy; + } + + /** + * Calculates the initial SoC-Energy of the next Period. + * + * @param ess the ess charge/discharge energy of the current Period + */ + public synchronized void calculateEssInitial(int ess) { + this.essInitialEnergy = max(0, this.essInitialEnergy - ess); // always at least '0' + } + + /** + * The initial SoC-Energy of the Period. + * + * @return the value + */ + public int getEssInitial() { + return this.essInitialEnergy; + } + + @Override + public String toString() { + return toStringHelper(this) // + .add("essInitialEnergy", this.essInitialEnergy) // + .addValue(this.global) // + .toString(); + } +} diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/package-info.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/package-info.java new file mode 100644 index 00000000000..479bc6029cb --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/simulation/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.energy.api.simulation; diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/AbstractDummyEnergySchedulable.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/AbstractDummyEnergySchedulable.java new file mode 100644 index 00000000000..0880e69eb33 --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/AbstractDummyEnergySchedulable.java @@ -0,0 +1,16 @@ +package io.openems.edge.energy.api.test; + +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.test.AbstractDummyOpenemsComponent; +import io.openems.edge.energy.api.EnergySchedulable; + +public abstract class AbstractDummyEnergySchedulable> + extends AbstractDummyOpenemsComponent implements EnergySchedulable, OpenemsComponent { + + protected AbstractDummyEnergySchedulable(String id, + io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, + io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) { + super(id, firstInitialChannelIds, furtherInitialChannelIds); + } + +} diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/DummyEnergySchedulable.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/DummyEnergySchedulable.java new file mode 100644 index 00000000000..53bac023549 --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/DummyEnergySchedulable.java @@ -0,0 +1,37 @@ +package io.openems.edge.energy.api.test; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.energy.api.EnergySchedulable; +import io.openems.edge.energy.api.EnergyScheduleHandler; + +/** + * Provides a simple, simulated {@link EnergySchedulable} component that can be + * used together with the OpenEMS Component test framework. + */ +public class DummyEnergySchedulable extends AbstractDummyEnergySchedulable + implements EnergySchedulable, OpenemsComponent { + + private final EnergyScheduleHandler esh; + + public DummyEnergySchedulable(String id, EnergyScheduleHandler esh) { + super(id, // + OpenemsComponent.ChannelId.values() // + ); + this.esh = esh; + } + + @Override + protected final DummyEnergySchedulable self() { + return this; + } + + @Override + public void run() throws OpenemsNamedException { + } + + @Override + public EnergyScheduleHandler getEnergyScheduleHandler() { + return this.esh; + } +} diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/DummyGlobalSimulationsContext.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/DummyGlobalSimulationsContext.java new file mode 100644 index 00000000000..8918dd8f5f1 --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/DummyGlobalSimulationsContext.java @@ -0,0 +1,99 @@ +package io.openems.edge.energy.api.test; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Arrays; + +import com.google.common.collect.ImmutableList; + +import io.openems.common.test.TimeLeapClock; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext.Period; + +public class DummyGlobalSimulationsContext { + + private DummyGlobalSimulationsContext() { + } + + public static final TimeLeapClock CLOCK = new TimeLeapClock(Instant.ofEpochSecond(946684800), ZoneId.of("UTC")); + public static final ZonedDateTime TIME = ZonedDateTime.now(CLOCK); + + /** + * Generates a {@link GlobalSimulationsContext} with the given + * {@link EnergyScheduleHandler}s. + * + * @param handlers the {@link EnergyScheduleHandler}s + * @return a {@link GlobalSimulationsContext} + */ + public static GlobalSimulationsContext fromHandlers(EnergyScheduleHandler... handlers) { + return new GlobalSimulationsContext(// + CLOCK, TIME, // + Arrays.stream(handlers).collect(toImmutableList()), // + new GlobalSimulationsContext.Grid(4000, 20000), // + new GlobalSimulationsContext.Ess(5000, 22000, 4000, 4000), // + ImmutableList.of(// + new Period.Quarter(time(0, 0), 0, 106, 293.70), // + new Period.Quarter(time(0, 15), 0, 86, 293.70), // + new Period.Quarter(time(0, 30), 0, 88, 293.70), // + new Period.Quarter(time(0, 45), 0, 81, 293.70), // + new Period.Quarter(time(1, 0), 0, 73, 294.30), // + new Period.Quarter(time(1, 15), 0, 68, 294.30), // + new Period.Quarter(time(1, 30), 0, 76, 294.30), // + new Period.Quarter(time(1, 45), 0, 149, 294.30), // + new Period.Quarter(time(2, 0), 0, 333, 289.30), // + new Period.Quarter(time(2, 15), 0, 61, 289.30), // + new Period.Quarter(time(2, 30), 0, 74, 289.30), // + new Period.Quarter(time(2, 45), 0, 73, 289.30), // + new Period.Quarter(time(3, 0), 0, 68, 288.00), // + new Period.Quarter(time(3, 15), 0, 66, 288.00), // + new Period.Quarter(time(3, 30), 0, 82, 288.00), // + new Period.Quarter(time(3, 45), 0, 99, 288.00), // + new Period.Quarter(time(4, 0), 0, 84, 288.80), // + new Period.Quarter(time(4, 15), 0, 80, 288.80), // + new Period.Quarter(time(4, 30), 0, 97, 288.80), // + new Period.Quarter(time(4, 45), 0, 85, 288.80), // + new Period.Quarter(time(5, 0), 0, 65, 302.90), // + new Period.Quarter(time(5, 15), 0, 69, 302.90), // + new Period.Quarter(time(5, 30), 0, 75, 302.90), // + new Period.Quarter(time(5, 45), 3, 90, 302.90), // + new Period.Quarter(time(6, 0), 6, 394, 331.70), // + new Period.Quarter(time(6, 15), 36, 106, 331.70), // + new Period.Quarter(time(6, 30), 112, 94, 331.70), // + new Period.Quarter(time(6, 45), 205, 74, 331.70), // + new Period.Quarter(time(7, 0), 342, 62, 342.50), // + new Period.Quarter(time(7, 15), 437, 74, 342.50), // + new Period.Quarter(time(7, 30), 518, 72, 342.50), // + new Period.Quarter(time(7, 45), 628, 60, 342.50), // + new Period.Quarter(time(8, 0), 931, 46, 332.70), // + new Period.Quarter(time(8, 15), 1159, 45, 332.70), // + new Period.Quarter(time(8, 30), 1349, 40, 332.70), // + new Period.Quarter(time(8, 45), 1543, 26, 332.70), // + new Period.Quarter(time(9, 0), 1743, 46, 311.80), // + new Period.Quarter(time(9, 15), 1920, 472, 311.80), // + new Period.Quarter(time(9, 30), 2112, 498, 311.80), // + new Period.Quarter(time(9, 45), 2209, 83, 311.80), // + new Period.Quarter(time(10, 0), 2436, 105, 292.10), // + new Period.Quarter(time(10, 15), 2671, 92, 292.10), // + new Period.Quarter(time(10, 30), 2723, 133, 292.10), // + new Period.Quarter(time(10, 45), 2824, 88, 292.10), // + new Period.Hour(time(11, 0), 11610, 716, 282.90, ImmutableList.of(// + new Period.Quarter(time(11, 0), 2878, 86, 282.90), // + new Period.Quarter(time(11, 15), 2871, 245, 282.90), // + new Period.Quarter(time(11, 30), 2883, 308, 282.90), // + new Period.Quarter(time(11, 45), 2978, 77, 282.90))), // + new Period.Hour(time(12, 0), 6118, 241, 260.70, ImmutableList.of(// + new Period.Quarter(time(12, 0), 3044, 54, 260.70), // + new Period.Quarter(time(12, 15), 3022, 64, 260.70), // + new Period.Quarter(time(12, 30), 3036, 64, 260.70), // + new Period.Quarter(time(12, 45), 3045, 59, 260.70))) // + )); + } + + private static ZonedDateTime time(int hours, int minutes) { + return TIME.plusHours(hours).plusMinutes(minutes); + } +} diff --git a/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/package-info.java b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/package-info.java new file mode 100644 index 00000000000..b8f87f83ce3 --- /dev/null +++ b/io.openems.edge.energy.api/src/io/openems/edge/energy/api/test/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.energy.api.test; diff --git a/io.openems.edge.energy.api/test/io/openems/edge/energy/api/EnergyUtilsTest.java b/io.openems.edge.energy.api/test/io/openems/edge/energy/api/EnergyUtilsTest.java new file mode 100644 index 00000000000..b4a6a7f2d69 --- /dev/null +++ b/io.openems.edge.energy.api/test/io/openems/edge/energy/api/EnergyUtilsTest.java @@ -0,0 +1,55 @@ +package io.openems.edge.energy.api; + +import static io.openems.edge.energy.api.EnergyUtils.findFirstPeakIndex; +import static io.openems.edge.energy.api.EnergyUtils.findFirstValleyIndex; +import static io.openems.edge.energy.api.EnergyUtils.interpolateArray; +import static io.openems.edge.energy.api.EnergyUtils.toPower; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class EnergyUtilsTest { + + @Test + public void testFindFirstPeakIndex() { + assertEquals(0, findFirstPeakIndex(0, new double[0])); + assertEquals(0, findFirstPeakIndex(0, new double[] { 1 })); + assertEquals(0, findFirstPeakIndex(0, new double[] { 1, 0 })); + assertEquals(1, findFirstPeakIndex(0, new double[] { 0, 1, 0 })); + assertEquals(1, findFirstPeakIndex(0, new double[] { 0, 1, 0, 1 })); + assertEquals(5, findFirstPeakIndex(5, new double[0])); + } + + @Test + public void testFindFirstValleyIndex() { + assertEquals(0, findFirstValleyIndex(0, new double[0])); + assertEquals(0, findFirstValleyIndex(0, new double[] { 1 })); + assertEquals(1, findFirstValleyIndex(0, new double[] { 1, 0 })); + assertEquals(0, findFirstValleyIndex(0, new double[] { 0, 1, 0 })); + assertEquals(2, findFirstValleyIndex(1, new double[] { 0, 1, 0, 1 })); + assertEquals(5, findFirstValleyIndex(5, new double[0])); + } + + @Test + public void testInterpolateArrayInteger() { + assertArrayEquals(new int[] { 123, 123, 234, 234, 345 }, // + interpolateArray(new Integer[] { null, 123, 234, null, 345, null })); + + assertArrayEquals(new int[] {}, // + interpolateArray(new Integer[] { null })); + + assertArrayEquals(new int[] { 123, 123 }, // + interpolateArray(new Integer[] { null, 123 })); + + assertArrayEquals(new int[] { 123 }, // + interpolateArray(new Integer[] { 123, null })); + } + + @Test + public void testToPower() { + assertEquals(2000, (int) toPower(500)); + assertNull(toPower(null)); + } +} diff --git a/io.openems.edge.energy.api/test/io/openems/edge/energy/api/simulation/GlobalSimulationsContextTest.java b/io.openems.edge.energy.api/test/io/openems/edge/energy/api/simulation/GlobalSimulationsContextTest.java new file mode 100644 index 00000000000..18eb644ba9c --- /dev/null +++ b/io.openems.edge.energy.api/test/io/openems/edge/energy/api/simulation/GlobalSimulationsContextTest.java @@ -0,0 +1,95 @@ +package io.openems.edge.energy.api.simulation; + +import static io.openems.edge.energy.api.simulation.GlobalSimulationsContext.calculatePeriodDurationHourFromIndex; +import static io.openems.edge.energy.api.simulation.GlobalSimulationsContext.generateProductionPrediction; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.test.TimeLeapClock; +import io.openems.edge.common.sum.DummySum; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.energy.api.EnergyConstants; +import io.openems.edge.predictor.api.prediction.Prediction; +import io.openems.edge.predictor.api.test.DummyPredictor; +import io.openems.edge.predictor.api.test.DummyPredictorManager; +import io.openems.edge.timeofusetariff.test.DummyTimeOfUseTariffProvider; + +public class GlobalSimulationsContextTest { + + private static final TimeLeapClock CLOCK = new TimeLeapClock(Instant.ofEpochSecond(946684800), ZoneId.of("UTC")); + + @Test + public void testBuild() throws OpenemsNamedException { + final var cm = new DummyComponentManager(CLOCK); + final var now = ZonedDateTime.now(CLOCK); + final var sum = new DummySum() // + .withEssCapacity(10000) // + .withEssSoc(50) // + .withEssMinDischargePower(-4000) // + .withEssMaxDischargePower(5000); + final var predictorManager = new DummyPredictorManager(// + new DummyPredictor("predictor0", cm, Prediction.from(sum, // + EnergyConstants.SUM_UNMANAGED_CONSUMPTION, now, new Integer[] { // + 4000, 8000, 6000, 2000, 3000, 5000, 7000, 9000, // + 4001, 8001, 6001, 2001, 3001, 5001, 7001, 9001, // + 4002, 8002, 6002, 2002, 3002, 5002, 7002, 9002, // + 4003, 8003, 6003, 2003, 3003, 5003, 7003, 9003, // + 4004, 8004, 6004, 2004, 3004, 5004, 7004, 9004, // + }), EnergyConstants.SUM_UNMANAGED_CONSUMPTION), + new DummyPredictor("predictor1", cm, Prediction.from(sum, // + EnergyConstants.SUM_PRODUCTION, now, + new Integer[] { 8000, 9000, 10000, 11000, 7000, 4000, 3000, 5000, // + 8001, 9001, 10001, 11001, 7001, 4001, 3001, 5001, // + 8002, 9002, 10002, 11002, 7002, 4002, 3002, 5002, // + 8003, 9003, 10003, 11003, 7003, 4003, 3003, 5003, // + 8004, 9004, 10004, 11004, 7004, 4004, 3004, 5004, // + }), EnergyConstants.SUM_PRODUCTION)); + final var prices = DummyTimeOfUseTariffProvider.fromQuarterlyPrices(CLOCK, // + 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, // + 11.1, 12.1, 13.1, 14.1, 15.1, 16.1, 17.1, 18.1, // + 11.2, 12.2, 13.2, 14.2, 15.2, 16.2, 17.2, 18.2, // + 11.3, 12.3, 13.3, 14.3, 15.3, 16.3, 17.3, 18.3, // + 11.4, 12.4, 13.4, 14.4, 15.4, 16.4, 17.4, 18.4 // + ); + + var gsc = GlobalSimulationsContext.create() // + .setClock(CLOCK) // + .setEnergyScheduleHandlers(ImmutableList.of()) // + .setSum(sum) // + .setPredictorManager(predictorManager) // + .setTimeOfUseTariff(prices) // + .build(); + + assertEquals(1000 /* -4000 W */, gsc.ess().maxChargeEnergy()); + assertEquals(1250 /* 5000 W */, gsc.ess().maxDischargeEnergy()); + assertEquals(28, gsc.periods().size()); + var p0 = gsc.periods().get(0); + assertEquals(2000 /* Wh */, p0.production()); + assertEquals(1000 /* Wh */, p0.consumption()); + } + + @Test + public void testGenerateProductionPrediction() { + final var arr = new Integer[] { 1, 2, 3 }; + assertArrayEquals(arr, generateProductionPrediction(arr, 2)); + assertArrayEquals(new Integer[] { 1, 2, 3, 0 }, generateProductionPrediction(arr, 4)); + } + + @Test + public void testCalculatePeriodDurationHourFromIndex() { + assertEquals(24, calculatePeriodDurationHourFromIndex(ZonedDateTime.parse("2020-03-04T14:00:00.00Z"))); + assertEquals(24 + 3, calculatePeriodDurationHourFromIndex(ZonedDateTime.parse("2020-03-04T14:15:00.00Z"))); + assertEquals(24 + 2, calculatePeriodDurationHourFromIndex(ZonedDateTime.parse("2020-03-04T14:30:00.00Z"))); + assertEquals(24 + 1, calculatePeriodDurationHourFromIndex(ZonedDateTime.parse("2020-03-04T14:45:00.00Z"))); + assertEquals(24, calculatePeriodDurationHourFromIndex(ZonedDateTime.parse("2020-03-04T15:00:00.00Z"))); + } +} diff --git a/io.openems.edge.energy/bnd.bnd b/io.openems.edge.energy/bnd.bnd index 7df50f07c5e..2b14c6014a9 100644 --- a/io.openems.edge.energy/bnd.bnd +++ b/io.openems.edge.energy/bnd.bnd @@ -3,20 +3,29 @@ Bundle-Vendor: FENECON GmbH Bundle-License: https://opensource.org/licenses/EPL-2.0 Bundle-Version: 1.0.0.${tstamp} +# TODO remove emergencycapacityreserve and limittotaldischarge from buildpath after v1 + -buildpath: \ ${buildpath},\ io.openems.common,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ io.openems.edge.controller.ess.emergencycapacityreserve,\ + io.openems.edge.controller.ess.fixactivepower,\ io.openems.edge.controller.ess.limittotaldischarge,\ io.openems.edge.controller.ess.timeofusetariff,\ io.openems.edge.energy.api,\ io.openems.edge.ess.api,\ io.openems.edge.predictor.api,\ + io.openems.edge.scheduler.api,\ io.openems.edge.timedata.api,\ io.openems.edge.timeofusetariff.api,\ io.openems.wrapper.jenetics,\ - + -testpath: \ - ${testpath} \ No newline at end of file + ${testpath},\ + io.openems.edge.controller.ess.emergencycapacityreserve,\ + io.openems.edge.controller.ess.gridoptimizedcharge,\ + io.openems.edge.controller.ess.limittotaldischarge,\ + io.openems.edge.controller.ess.timeofusetariff,\ + org.apache.commons.math3,\ diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/Config.java b/io.openems.edge.energy/src/io/openems/edge/energy/Config.java index 5b1344d5a72..1552f7dafa1 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/Config.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/Config.java @@ -3,6 +3,8 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import io.openems.edge.energy.api.Version; + @ObjectClassDefinition(// name = "Core Energy Scheduler", // description = "The global Energy Scheduler.") @@ -11,5 +13,11 @@ @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") boolean enabled() default true; + @AttributeDefinition(name = "Log-Verbosity", description = "The log verbosity") + LogVerbosity logVerbosity() default LogVerbosity.DEBUG_LOG; + + @AttributeDefinition(name = "Version", description = "Select version of implementation") + Version version() default Version.V1_ESS_ONLY; + String webconsole_configurationFactory_nameHint() default "Core Energy Scheduler"; } \ No newline at end of file diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/EnergySchedulerImpl.java b/io.openems.edge.energy/src/io/openems/edge/energy/EnergySchedulerImpl.java index 1f0e892b447..80d830c61cc 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/EnergySchedulerImpl.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/EnergySchedulerImpl.java @@ -1,10 +1,14 @@ package io.openems.edge.energy; -import static io.openems.edge.energy.optimizer.Utils.handleGetScheduleRequest; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static io.openems.common.utils.ThreadPoolUtils.shutdownAndAwaitTermination; +import static io.openems.edge.energy.optimizer.Utils.sortByScheduler; import java.time.ZonedDateTime; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -21,18 +25,24 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponse; 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.ComponentJsonApi; -import io.openems.edge.common.jsonapi.JsonApiBuilder; +import io.openems.edge.common.jsonapi.Call; import io.openems.edge.common.sum.Sum; -import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffController; import io.openems.edge.energy.api.EnergySchedulable; +import io.openems.edge.energy.api.EnergyScheduleHandler.AbstractEnergyScheduleHandler; import io.openems.edge.energy.api.EnergyScheduler; -import io.openems.edge.energy.jsonrpc.GetScheduleRequest; -import io.openems.edge.energy.optimizer.GlobalContext; +import io.openems.edge.energy.api.Version; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; import io.openems.edge.energy.optimizer.Optimizer; +import io.openems.edge.energy.v1.jsonrpc.GetScheduleResponse; +import io.openems.edge.energy.v1.optimizer.GlobalContextV1; +import io.openems.edge.energy.v1.optimizer.OptimizerV1; +import io.openems.edge.energy.v1.optimizer.UtilsV1; import io.openems.edge.predictor.api.manager.PredictorManager; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; @@ -43,11 +53,13 @@ immediate = true, // configurationPolicy = ConfigurationPolicy.OPTIONAL // ) -public class EnergySchedulerImpl extends AbstractOpenemsComponent - implements OpenemsComponent, EnergyScheduler, ComponentJsonApi { +@SuppressWarnings("deprecation") +public class EnergySchedulerImpl extends AbstractOpenemsComponent implements OpenemsComponent, EnergyScheduler { /** The hard working Optimizer. */ + private final OptimizerV1 optimizerV1; private final Optimizer optimizer; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); @Reference private ConfigurationAdmin cm; @@ -61,71 +73,132 @@ public class EnergySchedulerImpl extends AbstractOpenemsComponent @Reference(policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) private volatile TimeOfUseTariff timeOfUseTariff; + @Reference + private io.openems.edge.scheduler.api.Scheduler scheduler; + @Reference private Sum sum; + private final List schedulables = new CopyOnWriteArrayList<>(); + @Reference(policyOption = ReferencePolicyOption.GREEDY, // cardinality = ReferenceCardinality.MULTIPLE, // policy = ReferencePolicy.DYNAMIC, // target = "(enabled=true)") - private volatile List> schedulables = new CopyOnWriteArrayList<>(); + private void addSchedulable(EnergySchedulable schedulable) { + this.schedulables.add(schedulable); + var esh = (AbstractEnergyScheduleHandler) schedulable.getEnergyScheduleHandler(); // this is safe + esh.setOnRescheduleCallback(() -> this.optimizer.triggerReschedule()); + this.resetOptimizer(); + } + + @SuppressWarnings("unused") + private void removeSchedulable(EnergySchedulable schedulable) { + this.schedulables.remove(schedulable); + var esh = (AbstractEnergyScheduleHandler) schedulable.getEnergyScheduleHandler(); // this is safe + esh.removeOnRescheduleCallback(); + this.resetOptimizer(); + } @Reference(policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) private volatile Timedata timedata; + @Deprecated + @Reference(policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL, target = "(enabled=true)") + private volatile TimeOfUseTariffController timeOfUseTariffController; + + private Config config; + public EnergySchedulerImpl() { super(// OpenemsComponent.ChannelId.values(), // EnergyScheduler.ChannelId.values() // ); - // Prepare Optimizer and Context - this.optimizer = new Optimizer(() -> { - if (this.timeOfUseTariff == null) { - throw new OpenemsException("TimeOfUseTariff is not available"); - } - var ctrl = this.schedulables.stream() // - .filter(TimeOfUseTariffControllerImpl.class::isInstance) // - .map(TimeOfUseTariffControllerImpl.class::cast) // - .findFirst().orElse(null); - if (ctrl == null) { - throw new OpenemsException("TimeOfUseTariffController is not available"); - } - var esh = ctrl.getEnergyScheduleHandler(); - // NOTE: This is a workaround while we refactor TimeOfUseTariffController - // This code assumes that the `EnergySchedulable` is a - // `TimeOfUseTariffController` - return GlobalContext.create() // - .setClock(this.componentManager.getClock()) // - .setEnergyScheduleHandler(esh) // - .setSum(this.sum) // - .setPredictorManager(this.predictorManager) // - .setTimeOfUseTariff(this.timeOfUseTariff) // - .build(); - }); + + this.optimizerV1 = new OptimizerV1(// + () -> this.config.logVerbosity(), // + () -> { + if (this.timeOfUseTariff == null) { + throw new OpenemsException("TimeOfUseTariff is not available"); + } + if (this.timeOfUseTariffController == null) { + throw new OpenemsException("TimeOfUseTariffController is not available"); + } + return GlobalContextV1.create() // + .setClock(this.componentManager.getClock()) // + .setEnergyScheduleHandler(this.timeOfUseTariffController.getEnergyScheduleHandlerV1()) // + .setSum(this.sum) // + .setPredictorManager(this.predictorManager) // + .setTimeOfUseTariff(this.timeOfUseTariff) // + .build(); + }); + + this.optimizer = new Optimizer(// + () -> this.config.logVerbosity(), // + () -> { + // Sort Schedulables by the order in the Scheduler + var schedulables = sortByScheduler(this.scheduler, this.schedulables); + var eshs = schedulables.stream() // + .map(EnergySchedulable::getEnergyScheduleHandler) // + .collect(toImmutableList()); + + return GlobalSimulationsContext.create() // + .setClock(this.componentManager.getClock()) // + .setEnergyScheduleHandlers(eshs) // + .setSum(this.sum) // + .setPredictorManager(this.predictorManager) // + .setTimeOfUseTariff(this.timeOfUseTariff) // + .build(); + }, // + this.channel(EnergyScheduler.ChannelId.SIMULATIONS_PER_QUARTER)); } @Activate private void activate(ComponentContext context, Config config) throws OpenemsException { super.activate(context, SINGLETON_COMPONENT_ID, SINGLETON_SERVICE_PID, true); + if (this.applyConfig(config)) { - this.optimizer.activate(this.id()); + switch (config.version()) { + case V1_ESS_ONLY -> this.optimizerV1.activate(this.id()); + case V2_ENERGY_SCHEDULABLE -> this.executor.execute(this.optimizer); + } } } @Modified private void modified(ComponentContext context, Config config) throws OpenemsNamedException { super.modified(context, SINGLETON_COMPONENT_ID, SINGLETON_SERVICE_PID, true); - if (this.applyConfig(config)) { - this.optimizer.modified(this.id()); + this.applyConfig(config); + } + + private void resetOptimizer() { + if (this.config == null) { + return; // Wait for @Activate + } + switch (this.config.version()) { + case V1_ESS_ONLY -> this.optimizerV1.activate(this.id()); + case V2_ENERGY_SCHEDULABLE -> this.optimizer.triggerReschedule(); } } + @Override + public String debugLog() { + if (this.config != null && this.config.version() == Version.V2_ENERGY_SCHEDULABLE) { + return this.optimizer.debugLog(); + } + return null; + } + private synchronized boolean applyConfig(Config config) { + this.config = config; if (OpenemsComponent.validateSingleton(this.cm, SINGLETON_SERVICE_PID, SINGLETON_COMPONENT_ID)) { return false; } - if (!config.enabled()) { + if (config.enabled()) { + this.resetOptimizer(); + } else { + this.optimizerV1.deactivate(); this.optimizer.deactivate(); return false; } @@ -136,14 +209,18 @@ private synchronized boolean applyConfig(Config config) { @Override @Deactivate protected void deactivate() { + this.optimizerV1.deactivate(); this.optimizer.deactivate(); + shutdownAndAwaitTermination(this.executor, 0); super.deactivate(); } @Override - public void buildJsonApiRoutes(JsonApiBuilder builder) { - builder.handleRequest(GetScheduleRequest.METHOD, call -> handleGetScheduleRequest(// - this.optimizer, call.getRequest().getId(), this.timedata, this.timeOfUseTariff, - "ctrlEssTimeOfUseTariff0", ZonedDateTime.now(this.componentManager.getClock()))); + public GetScheduleResponse handleGetScheduleRequestV1(Call call, String id) { + if (this.optimizerV1 != null) { + return UtilsV1.handleGetScheduleRequest(this.optimizerV1, call.getRequest().getId(), this.timedata, + this.timeOfUseTariff, id, ZonedDateTime.now(this.componentManager.getClock())); + } + throw new IllegalArgumentException("This should have been Version V1"); } } diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/LogVerbosity.java b/io.openems.edge.energy/src/io/openems/edge/energy/LogVerbosity.java new file mode 100644 index 00000000000..27f2d942c3b --- /dev/null +++ b/io.openems.edge.energy/src/io/openems/edge/energy/LogVerbosity.java @@ -0,0 +1,13 @@ +package io.openems.edge.energy; + +public enum LogVerbosity { + NONE, + /** + * Show basic information in Controller.Debug.Log. + */ + DEBUG_LOG, + /** + * Trace. + */ + TRACE; +} diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Optimizer.java b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Optimizer.java index 030b0c3e4dc..63ca31e3315 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Optimizer.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Optimizer.java @@ -1,145 +1,251 @@ package io.openems.edge.energy.optimizer; -import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap; -import static io.openems.edge.energy.optimizer.Simulator.simulate; +import static io.jenetics.engine.Limits.byExecutionTime; +import static io.openems.common.utils.DateUtils.roundDownToQuarter; +import static io.openems.edge.energy.optimizer.QuickSchedules.findBestQuickSchedule; import static io.openems.edge.energy.optimizer.Utils.calculateExecutionLimitSeconds; -import static io.openems.edge.energy.optimizer.Utils.createSimulatorParams; +import static io.openems.edge.energy.optimizer.Utils.createSimulator; import static io.openems.edge.energy.optimizer.Utils.initializeRandomRegistryForProduction; -import static io.openems.edge.energy.optimizer.Utils.logSchedule; -import static io.openems.edge.energy.optimizer.Utils.updateSchedule; -import static java.lang.Thread.sleep; +import static io.openems.edge.energy.optimizer.Utils.logSimulationResult; +import static java.time.Duration.ofSeconds; -import java.time.Duration; -import java.time.Instant; +import java.time.Clock; import java.time.ZonedDateTime; -import java.util.Map.Entry; -import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.collect.ImmutableSortedMap; - import io.openems.common.exceptions.OpenemsException; import io.openems.common.function.ThrowingSupplier; -import io.openems.common.test.TimeLeapClock; -import io.openems.common.worker.AbstractImmediateWorker; +import io.openems.common.utils.FunctionUtils; +import io.openems.edge.common.channel.Channel; +import io.openems.edge.energy.LogVerbosity; import io.openems.edge.energy.api.EnergyScheduleHandler; -import io.openems.edge.energy.optimizer.Simulator.Period; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; /** * This task is executed once in the beginning and afterwards every full 15 * minutes. */ -public class Optimizer extends AbstractImmediateWorker { +public class Optimizer implements Runnable { private final Logger log = LoggerFactory.getLogger(Optimizer.class); - private final ThrowingSupplier globalContext; - private final TreeMap schedule = new TreeMap<>(); + private final Supplier logVerbosity; + private final ThrowingSupplier gscSupplier; + private final Channel simulationsPerQuarterChannel; + private final AtomicBoolean interruptFlag = new AtomicBoolean(false); - private Params params = null; + private Simulator simulator = null; + private SimulationResult simulationResult = SimulationResult.EMPTY; - public Optimizer(ThrowingSupplier globalContext) { - this.globalContext = globalContext; + public Optimizer(Supplier logVerbosity, + ThrowingSupplier gscSupplier, // + Channel simulationsPerQuarterChannel) { + this.logVerbosity = logVerbosity; + this.gscSupplier = gscSupplier; + this.simulationsPerQuarterChannel = simulationsPerQuarterChannel; initializeRandomRegistryForProduction(); + } - // Run Optimizer thread in LOW PRIORITY - this.setPriority(Thread.MIN_PRIORITY); + /** + * Deactivate the {@link Optimizer}. + */ + public synchronized void deactivate() { + this.interruptFlag.set(true); + } + + /** + * Triggers Rescheduling. + */ + public void triggerReschedule() { + this.traceLog(() -> "Trigger Reschedule"); + this.interruptFlag.set(true); } @Override - public void forever() throws InterruptedException, OpenemsException { - this.log.info("# Start next run of Optimizer"); + public void run() { + try { + while (true) { + this.traceLog(() -> "Run..."); + this.interruptFlag.set(false); + + // Create the Simulator with GlobalSimulationsContext + createSimulator(this.gscSupplier, this.interruptFlag, // + simulator -> this.simulator = simulator, // + error -> { + this.traceLog(error); + this.applyEmptySimulationResult(); + }); + this.traceLog(() -> "Simulator is " + this.simulator); + final var simulator = this.simulator; + if (simulator == null) { + continue; + } - this.createParams(); // this possibly takes forever + this.runOnce(simulator); + } + } catch (InterruptedException | ExecutionException e) { + this.log.error("OPTIMIZER execution failed InterruptedException|ExecutionException: " + e.getMessage()); + e.printStackTrace(); + + // ignore + } catch (Exception e) { + this.log.error("OPTIMIZER execution failed: " + e.getMessage()); + e.printStackTrace(); + } + } - final var globalContext = this.globalContext.get(); - final var start = Instant.now(globalContext.clock()); + /** + * Run the optimization once. + * + * @param simulator the {@link Simulator} + * @throws InterruptedException on error + * @throws ExecutionException on error + */ + public void runOnce(Simulator simulator) throws InterruptedException, ExecutionException { + if (this.simulationResult == SimulationResult.EMPTY) { + // No Schedule available yet. Start with a default Schedule with all States + // set to default. + this.traceLog(() -> "No existing schedule available -> apply default"); + this.applyBestQuickSchedule(simulator); + } - long executionLimitSeconds; + this.traceLog(() -> "Run Simulation..."); - // Calculate max execution time till next quarter (with buffer) - executionLimitSeconds = calculateExecutionLimitSeconds(globalContext.clock()); + var simulationResult = this.runSimulation(simulator).get(); + if (simulationResult == null/* no result */ || this.interruptFlag.get() /* was interrupted */) { + this.traceLog(() -> "Simulation gave no result or was interrupted!"); + this.simulationsPerQuarterChannel.setNextValue(null); + this.applyBestQuickSchedule(simulator); + return; + } - // Find best Schedule - var schedule = Simulator.getBestSchedule(this.params, executionLimitSeconds); + this.traceLog(() -> "Calculate metrics"); - // Re-Simulate and keep best Schedule - var newSchedule = simulate(this.params, schedule); + // Calculate metrics + var stats = simulator.cache.stats(); + this.simulationsPerQuarterChannel.setNextValue(stats.loadCount()); - // Debug Log best Schedule - logSchedule(this.params, newSchedule); + // Apply simulation result to EnergyScheduleHandlers + this.applySimulationResult(simulator, simulationResult, false); + } - // Update Schedule from newly simulated Schedule - synchronized (this.schedule) { - updateSchedule(ZonedDateTime.now(globalContext.clock()), this.schedule, newSchedule); - } + private CompletableFuture runSimulation(Simulator simulator) { + this.traceLog(() -> "Run next Simulation"); + return CompletableFuture.supplyAsync(() -> { + this.traceLog(() -> "Executing async Simulation"); - // Send Schedule to Controller - globalContext.energyScheduleHandler().setSchedule(this.schedule.entrySet().stream()// - .collect(toImmutableMap(// - Entry::getKey, // - e -> new EnergyScheduleHandler.Period<>(e.getValue().state(), - e.getValue().op().essChargeInChargeGrid())))); - - // Sleep remaining time - if (!(globalContext.clock() instanceof TimeLeapClock)) { - var remainingExecutionLimit = Duration - .between(Instant.now(globalContext.clock()), start.plusSeconds(executionLimitSeconds)).getSeconds(); - if (remainingExecutionLimit > 0) { - this.log.info("Sleep [" + remainingExecutionLimit + "s] till next run of Optimizer"); - sleep(remainingExecutionLimit * 1000); - } - } + final var executionLimit = byExecutionTime(ofSeconds(calculateExecutionLimitSeconds())); + + // Find best Schedule + var bestSchedule = simulator.getBestSchedule(this.simulationResult, null, // + stream -> stream // + // Stop on interruptFlag + .limit(ignore -> !this.interruptFlag.get()) // + // Stop till next quarter + .limit(executionLimit)); + + return bestSchedule; + }); } /** - * Try forever till all data is available (e.g. ESS Capacity) + * Create and apply the best quickly available Schedule. * - * @throws InterruptedException during sleep + * @param simulator the {@link Simulator} */ - private void createParams() throws InterruptedException { - while (true) { - try { - synchronized (this.schedule) { - this.params = createSimulatorParams(this.globalContext.get(), // - this.schedule.entrySet().stream() // - .collect(toImmutableSortedMap(// - ZonedDateTime::compareTo, // - Entry::getKey, e -> e.getValue().state()))); - return; - } + protected synchronized void applyBestQuickSchedule(Simulator simulator) { + // Find Genotype with lowest cost + var bestGt = findBestQuickSchedule(simulator, this.simulationResult); + if (bestGt == null) { + this.applyEmptySimulationResult(); + return; + } + var simulationResult = SimulationResult.fromQuarters(simulator.gsc, bestGt); - } catch (OpenemsException e) { - this.log.info("# Stuck trying to get Params. " + e.getMessage()); - this.params = null; - synchronized (this.schedule) { - this.schedule.clear(); - } - sleep(30_000); - } + this.traceLog(() -> "Applying best quick Schedule"); + this.applySimulationResult(simulator, simulationResult, true); + } + + private void applyEmptySimulationResult() { + this.traceLog(() -> "Applying empty Schedule"); + this.applySimulationResult(null, SimulationResult.EMPTY, true); + } + + /** + * Applies the Schedule to all {@link EnergyScheduleHandler}s and stores the + * {@link SimulationResult} in `this.simulationResult`. + * + * @param simulator the {@link Simulator}, possibly null + * @param simulationResult the {@link SimulationResult} + * @param updateActiveQuarter should the currently active quarter also get + * updated + */ + private void applySimulationResult(Simulator simulator, SimulationResult simulationResult, + boolean updateActiveQuarter) { + final Clock clock; + if (simulator != null) { + // Debug Log best Schedule + logSimulationResult(simulator, simulationResult); + clock = simulator.gsc.clock(); + } else { + clock = Clock.systemDefaultZone(); + } + + final var thisQuarter = roundDownToQuarter(ZonedDateTime.now(clock)); + final var nextQuarter = thisQuarter.plusMinutes(15); + + // Store result + this.simulationResult = simulationResult; + + // Send Schedule to Controllers + simulationResult.schedules().forEach((esh, schedule) -> { + esh.applySchedule(schedule // + .tailMap(updateActiveQuarter // + ? thisQuarter // update also current quarter + : nextQuarter)); // otherwise -> start with next quarter + }); + } + + private void traceLog(Supplier message) { + switch (this.logVerbosity.get()) { + case NONE, DEBUG_LOG -> FunctionUtils.doNothing(); + case TRACE -> this.log.info("OPTIMIZER " + message.get()); } } /** - * Gets the current {@link Params} or null. + * Gets the {@link SimulationResult}. * - * @return the {@link Params} or null + * @return {@link SimulationResult} */ - public Params getParams() { - return this.params; + public SimulationResult getSimulationResult() { + return this.simulationResult; } /** - * Gets a copy of the Schedule. - * - * @return {@link ImmutableSortedMap} + * Output for Controller.Debug.Log. + * + * @return the debug log output */ - public ImmutableSortedMap getSchedule() { - synchronized (this.schedule) { - return ImmutableSortedMap.copyOf(this.schedule); + public String debugLog() { + var b = new StringBuilder(); + if (this.simulationResult.periods().isEmpty()) { + b.append("No Schedule available"); + } else { + b.append("ScheduledPeriods:" + this.simulationResult.periods().size()); + } + var simulator = this.simulator; + if (simulator != null) { + var stats = simulator.cache.stats(); + b.append("|SimulationCounter:" + stats.loadCount()); } + b.append("|PerQuarter:" + this.simulationsPerQuarterChannel.value()); + return b.toString(); } } diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/QuickSchedules.java b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/QuickSchedules.java new file mode 100644 index 00000000000..57dfa9cf80f --- /dev/null +++ b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/QuickSchedules.java @@ -0,0 +1,217 @@ +package io.openems.edge.energy.optimizer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import com.google.common.collect.ImmutableSortedMap; + +import io.jenetics.Genotype; +import io.jenetics.IntegerChromosome; +import io.jenetics.IntegerGene; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; + +/** + * This class helps finding good Schedules that are quickly available. + */ +public class QuickSchedules { + + private QuickSchedules() { + } + + /** + * Finds the best quick Schedule, i.e. the one with the lowest cost. + * + * @param simulator the {@link Simulator} + * @param simulationResult the existing {@link SimulationResult}, or null + * @return the winner {@link Genotype}; or null + */ + public static Genotype findBestQuickSchedule(Simulator simulator, SimulationResult simulationResult) { + double lowestCost = 0.; + Genotype bestGt = null; + for (var gt : generateQuickSchedules(simulator.gsc, simulationResult)) { + var cost = simulator.calculateCost(gt); + if (bestGt == null || cost < lowestCost) { + bestGt = gt; + lowestCost = cost; + } + } + return bestGt; + } + + /** + * Generate quick Schedules. + * + * @param gsc the {@link GlobalSimulationsContext} + * @param simulationResult the existing {@link SimulationResult}, or null + * @return a List of {@link Genotype}s, entries can be null + */ + public static List> generateQuickSchedules(GlobalSimulationsContext gsc, + SimulationResult simulationResult) { + return Stream // + .concat(// + variationsOfAllStatesDefault(gsc), // + variationsFromExistingSimulationResult(gsc, simulationResult)) // + .filter(Objects::nonNull) // + .distinct() // + .toList(); + } + + /** + * Builds {@link Genotype}s with all states default and all possible variations + * for first period. + * + * @param gsc the {@link GlobalSimulationsContext} + * @return a Stream of {@link Genotype}s or nulls + */ + protected static Stream> variationsOfAllStatesDefault(GlobalSimulationsContext gsc) { + return generateAllCombinations(gsc).stream() // + .map(combination -> toGenotypeOrNull(gsc.handlers().stream() // + .filter(EnergyScheduleHandler.WithDifferentStates.class::isInstance) // + .map(EnergyScheduleHandler.WithDifferentStates.class::cast) // + .map(esh -> { + final var defaultState = esh.getDefaultStateIndex(); + final var noOfStates = esh.getAvailableStates().length; + final var firstState = combination.get(esh); + return IntegerChromosome.of(IntStream.range(0, gsc.periods().size()) // + .map(i -> i == 0 // + ? firstState // first period + : defaultState) // remaining periods + .mapToObj(state -> IntegerGene.of(state, 0, noOfStates)) // + .toList()); + }) // + .toList())); + } + + /** + * Builds {@link Genotype}s with all states from an existing + * {@link SimulationResult} and all possible variations for first period. + * + * @param gsc the {@link GlobalSimulationsContext} + * @param simulationResult the {@link SimulationResult} + * @return a Stream of {@link Genotype}s or nulls + */ + protected static Stream> variationsFromExistingSimulationResult(GlobalSimulationsContext gsc, + SimulationResult simulationResult) { + return generateAllCombinations(gsc).stream() // + .map(combination -> toGenotypeOrNull(gsc.handlers().stream() // + .filter(EnergyScheduleHandler.WithDifferentStates.class::isInstance) // + .map(EnergyScheduleHandler.WithDifferentStates.class::cast) // + .map(esh -> { + final var firstState = combination.get(esh); + final var existingSchedule = simulationResult.schedules().getOrDefault(esh, + ImmutableSortedMap.of()); + final var defaultState = esh.getDefaultStateIndex(); + final var noOfStates = esh.getAvailableStates().length; + return IntegerChromosome.of(IntStream.range(0, gsc.periods().size()) // + .map(i -> { + if (i == 0) { // + return firstState; // first period + } + // remaining periods + var period = gsc.periods().get(i); + var previousState = existingSchedule.get(period.time()); + if (previousState != null) { + return previousState.stateIndex(); + } + return defaultState; + }) // + .mapToObj(state -> IntegerGene.of(state, 0, noOfStates)) // + .toList()); + }) // + .toList())); + } + + /** + * Builds a {@link Genotype} of an existing {@link SimulationResult}. + * + * @param gsc the {@link GlobalSimulationsContext} + * @param simulationResult the {@link SimulationResult} + * @return the {@link Genotype} or null + */ + protected static Genotype fromExistingSimulationResult(GlobalSimulationsContext gsc, + SimulationResult simulationResult) { + if (simulationResult == null) { + return null; + } + return toGenotypeOrNull(gsc.handlers().stream() // + .filter(EnergyScheduleHandler.WithDifferentStates.class::isInstance) // + .map(EnergyScheduleHandler.WithDifferentStates.class::cast) // + .map(esh -> { + final var existingSchedule = simulationResult.schedules().getOrDefault(esh, + ImmutableSortedMap.of()); + final var defaultState = esh.getDefaultStateIndex(); + final var noOfStates = esh.getAvailableStates().length; + return IntegerChromosome.of(gsc.periods().stream() // + .map(p -> { + var previousState = existingSchedule.get(p.time()); + var state = previousState == null // + ? defaultState // + : previousState.stateIndex(); + return IntegerGene.of(state, 0, noOfStates); + }) // + .toList()); + }) // + .toList()); + } + + private static Genotype toGenotypeOrNull(List cs) { + if (cs.isEmpty()) { + return null; + } + return Genotype.of(cs); + } + + /** + * Generates all possible combinations of + * {@link EnergyScheduleHandler.WithDifferentStates} and state-index for a + * Period. + * + * @param gsc {@link GlobalSimulationsContext} + * @return combinations + */ + @SuppressWarnings("rawtypes") + private static List> generateAllCombinations( + GlobalSimulationsContext gsc) { + if (gsc == null) { + return List.of(); + } + + var eshs = gsc.handlers().stream() // + .filter(EnergyScheduleHandler.WithDifferentStates.class::isInstance) // + .map(EnergyScheduleHandler.WithDifferentStates.class::cast) // + .toList(); + + List> result = new ArrayList<>(); + generateCombinationsRecursive(eshs, 0, new HashMap<>(), result); + return result; + } + + @SuppressWarnings("rawtypes") + private static void generateCombinationsRecursive(List inputList, + int index, Map currentCombination, + List> result) { + // Base case: If we've added a combination for each input, add the result to the + // list. + if (index == inputList.size()) { + result.add(new HashMap<>(currentCombination)); // Add a copy of the current map + return; + } + + // Get the current input + var currentInput = inputList.get(index); + + // Loop through all possible values for this input, from 0 to maxValue + for (int value = 0; value < currentInput.getAvailableStates().length; value++) { + currentCombination.put(currentInput, value); // Set this value in the map + // Recur to the next input + generateCombinationsRecursive(inputList, index + 1, currentCombination, result); + currentCombination.remove(currentInput); // Backtrack + } + } +} diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/SimulationResult.java b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/SimulationResult.java new file mode 100644 index 00000000000..fdcfe53001b --- /dev/null +++ b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/SimulationResult.java @@ -0,0 +1,177 @@ +package io.openems.edge.energy.optimizer; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Locale; +import java.util.function.BiConsumer; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; + +import io.jenetics.Genotype; +import io.jenetics.IntegerChromosome; +import io.jenetics.IntegerGene; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.simulation.EnergyFlow; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext.Period.Hour; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext.Period.Quarter; +import io.openems.edge.energy.optimizer.Simulator.EshToState; + +public record SimulationResult(// + double cost, // + ImmutableMap periods, // + ImmutableMap, // + ImmutableSortedMap> schedules) { + + /** + * A Period in a {@link SimulationResult}. Duration of one period is always one + * quarter. + */ + public record Period(// + GlobalSimulationsContext.Period context, // + EnergyFlow energyFlow, // + int essInitialEnergy // + ) { + + /** + * Constructor for {@link Period}. + * + * @param context the {@link GlobalSimulationsContext} + * @param energyFlow the {@link EnergyFlow} + * @param essInitialEnergy the initial ESS energy in the beginning of the period + * in [Wh] + * @return a {@link Period} + */ + public static Period from(GlobalSimulationsContext.Period context, EnergyFlow energyFlow, + int essInitialEnergy) { + return new Period(context, energyFlow, essInitialEnergy); + } + } + + /** + * An empty {@link SimulationResult}. + */ + public static final SimulationResult EMPTY = new SimulationResult(0., ImmutableMap.of(), ImmutableMap.of()); + + /** + * Re-Simulate a {@link Genotype} to create a {@link SimulationResult}. + * + * @param cache the {@link GenotypeCache} + * @param gsc the {@link GlobalSimulationsContext} + * @param gt the {@link Genotype} + * @return the {@link SimulationResult} + */ + private static SimulationResult from(GlobalSimulationsContext gsc, Genotype gt) { + var allPeriods = ImmutableMap.builder(); + var allEshToStates = new ArrayList(); + var cost = Simulator.simulate(gsc, gt, new Simulator.BestScheduleCollector(// + p -> allPeriods.put(p.context().time(), p), // + allEshToStates::add)); + + var schedules = allEshToStates.stream() // + .collect(toImmutableMap(EshToState::esh, // + eshToState -> ImmutableSortedMap.of(eshToState.period().context.time(), + new EnergyScheduleHandler.WithDifferentStates.Period.Transition( + eshToState.postProcessedStateIndex(), eshToState.period().context.price(), + eshToState.period().energyFlow, eshToState.period().essInitialEnergy)), + (a, b) -> ImmutableSortedMap.naturalOrder() + .putAll(a).putAll(b).build())); + + return new SimulationResult(cost, allPeriods.build(), schedules); + } + + /** + * Re-Simulate a {@link Genotype} to create a {@link SimulationResult}. + * + *

+ * This method re-simulates using the {@link Quarter} periods and not (only) the + * {@link Hour} periods. + * + * @param gsc the {@link GlobalSimulationsContext} + * @param gt the {@link Genotype} + * @return the {@link SimulationResult} + */ + public static SimulationResult fromQuarters(GlobalSimulationsContext gsc, Genotype gt) { + if (gsc == null || gt == null) { + return SimulationResult.EMPTY; + } + + // Convert to Quarters + final GlobalSimulationsContext quarterGsc; + final Genotype quarterGt; + { + final var quarterPeriods = ImmutableList.builder(); + final var quarterGenes = gt.stream().map(ignore -> ImmutableList.builder()).toList(); + final BiConsumer add = (j, p) -> { + quarterPeriods.add(p); + for (var i = 0; i < quarterGenes.size(); i++) { + quarterGenes.get(i).add(gt.get(i).get(j)); + } + }; + for (var i = 0; i < gsc.periods().size(); i++) { + var p = gsc.periods().get(i); + if (p instanceof GlobalSimulationsContext.Period.Quarter pq) { + add.accept(i, pq); + } else if (p instanceof GlobalSimulationsContext.Period.Hour ph) { + for (var j = 0; j < ph.quarterPeriods().size(); j++) { + var pq = ph.quarterPeriods().get(j); + add.accept(i, pq); + } + } + } + quarterGsc = new GlobalSimulationsContext(gsc.clock(), gsc.startTime(), gsc.handlers(), gsc.grid(), + gsc.ess(), quarterPeriods.build()); + quarterGt = Genotype.of(quarterGenes.stream() // + .map(gs -> IntegerChromosome.of(gs.build())) // + .toList()); + } + return from(quarterGsc, quarterGt); + } + + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + + private static void log(StringBuilder b, String format, Object... args) { + b.append(String.format(Locale.ENGLISH, format, args).toString()); + } + + /** + * Builds a log string of this {@link SimulationResult}. + * + * @return log string + */ + public String toLogString(String prefix) { + var b = new StringBuilder(prefix) // + .append("Time Price Production Consumption Ess Grid ProdToCons ProdToGrid ProdToEss GridToCons GridToEss EssToCons EssInitial\n"); + this.periods.entrySet().forEach(e -> { + final var time = e.getKey(); + final var p = e.getValue(); + final var c = p.context; + final var ef = p.energyFlow; + log(b, "%s", prefix); + log(b, "%s ", time.format(TIME_FORMATTER)); + log(b, "%6.2f ", c.price()); + log(b, "%10d ", ef.getProd()); + log(b, "%10d ", ef.getCons()); + log(b, "%6d ", ef.getEss()); + log(b, "%6d ", ef.getGrid()); + log(b, "%10d ", ef.getProdToCons()); + log(b, "%10d ", ef.getProdToGrid()); + log(b, "%9d ", ef.getProdToEss()); + log(b, "%10d ", ef.getGridToCons()); + log(b, "%9d ", ef.getGridToEss()); + log(b, "%9d ", ef.getEssToCons()); + log(b, "%10d ", p.essInitialEnergy); + this.schedules.forEach((esh, schedule) -> { + log(b, "%-15s ", esh.toStateString(schedule.get(time).stateIndex())); + }); + b.append("\n"); + }); + return b.toString(); + } +} diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Simulator.java b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Simulator.java index 0b79a5f6cc2..7c184779768 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Simulator.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Simulator.java @@ -1,177 +1,279 @@ package io.openems.edge.energy.optimizer; +import static com.google.common.base.MoreObjects.toStringHelper; import static io.jenetics.engine.EvolutionResult.toBestGenotype; -import static io.jenetics.engine.Limits.byExecutionTime; -import static io.openems.edge.energy.optimizer.InitialPopulationUtils.buildInitialPopulation; -import static io.openems.edge.energy.optimizer.Utils.paramsAreValid; -import static io.openems.edge.energy.optimizer.Utils.postprocessSimulatorState; -import static java.lang.Math.max; -import static java.time.Duration.ofSeconds; - -import java.time.ZonedDateTime; -import java.util.concurrent.atomic.AtomicInteger; +import static io.openems.edge.energy.optimizer.QuickSchedules.fromExistingSimulationResult; +import static java.lang.Thread.currentThread; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; import java.util.function.Consumer; import java.util.function.Function; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSortedMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import io.jenetics.Genotype; import io.jenetics.IntegerChromosome; import io.jenetics.IntegerGene; import io.jenetics.engine.Engine; -import io.jenetics.engine.EvolutionResult; -import io.openems.edge.controller.ess.timeofusetariff.StateMachine; -import io.openems.edge.energy.optimizer.Params.Length; -import io.openems.edge.energy.optimizer.Params.OptimizePeriod; +import io.jenetics.engine.EvolutionStream; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.EnergyScheduleHandler.AbstractEnergyScheduleHandler; +import io.openems.edge.energy.api.simulation.EnergyFlow; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; +import io.openems.edge.energy.api.simulation.OneSimulationContext; public class Simulator { /** Used to incorporate charge/discharge efficiency. */ public static final double EFFICIENCY_FACTOR = 1.17; - public record Period(OptimizePeriod op, StateMachine state, int essInitial, EnergyFlow ef) { + private static final Logger LOG = LoggerFactory.getLogger(Simulator.class); + + public final GlobalSimulationsContext gsc; + + protected final LoadingCache, Double> cache; + + public Simulator(GlobalSimulationsContext gsc) { + this.gsc = gsc; + this.cache = CacheBuilder.newBuilder() // + .recordStats() // + .build(new CacheLoader, Double>() { + + @Override + /** + * Simulates a Schedule and calculates the cost. + * + *

+ * NOTE: do not throw an Exception here, because we use + * {@link LoadingCache#getUnchecked(Object)} below. + * + * @param gt the Schedule as a {@link Genotype} + * @return the cost, lower is better, always positive; {@link Double#NaN} on + * error + */ + public Double load(final Genotype gt) { + return simulate(Simulator.this.gsc, gt, null); + } + }); + + // Initialize the EnergyScheduleHandlers. + for (var esh : gsc.handlers()) { + ((AbstractEnergyScheduleHandler) esh /* this is safe */).initialize(gsc); + } } /** * Simulates a Schedule and calculates the cost. * - * @param p the {@link Params} - * @param schedule the {@link StateMachine} states of the Schedule - * @return the cost, lower is better; always positive + *

+ * This method internally uses a Cache for {@link Genotype}s. + * + * @param gt the Schedule as a {@link Genotype} + * @return the cost, lower is better, always positive; {@link Double#NaN} on + * error */ - protected static double calculateCost(Params p, StateMachine[] schedule) { - final var nextEssInitial = new AtomicInteger(p.essInitialEnergy()); - var sum = 0.; - for (var i = 0; i < p.optimizePeriods().size(); i++) { - sum += simulatePeriod(p, p.optimizePeriods().get(i), schedule[i], nextEssInitial, null); - } - return sum; + public double calculateCost(Genotype gt) { + return this.cache.getUnchecked(gt); } /** - * Simulates a Schedule in quarterly periods. + * Simulates a Schedule and calculates the cost. + * + *

+ * This method does not a Cache for {@link Genotype}s. * - * @param p the {@link Params} - * @param schedule the {@link StateMachine} states of the Schedule - * @return a Map of {@link Period}s + * @param gt the simulated {@link Genotype} + * @param bestScheduleCollector the {@link BestScheduleCollector} + * @return the cost, lower is better, always positive; + * {@link Double#POSITIVE_INFINITY} on error */ - protected static ImmutableSortedMap simulate(Params p, StateMachine[] schedule) { - final var nextEssInitial = new AtomicInteger(p.essInitialEnergy()); - var result = ImmutableSortedMap.naturalOrder(); - for (var i = 0; i < p.optimizePeriods().size(); i++) { - var state = schedule[i]; - var op = p.optimizePeriods().get(i); - var length = op.quarterPeriods().size() == 1 ? Length.QUARTER : Length.HOUR; - // Convert mixed OptimizePeriods to pure quarterly - for (var qp : op.quarterPeriods()) { - var quarterlyOp = new OptimizePeriod(qp.time(), length, qp.essMaxChargeEnergy(), - qp.essMaxDischargeEnergy(), qp.essChargeInChargeGrid(), qp.maxBuyFromGrid(), qp.production(), - qp.consumption(), qp.price(), ImmutableList.of(qp)); - simulatePeriod(p, quarterlyOp, state, nextEssInitial, period -> result.put(period.op().time(), period)); - } + public double simulate(Genotype gt, BestScheduleCollector bestScheduleCollector) { + return simulate(this.gsc, gt, bestScheduleCollector); + } + + protected static double simulate(GlobalSimulationsContext gsc, Genotype gt, + BestScheduleCollector bestScheduleCollector) { + final var osc = OneSimulationContext.from(gsc); + final var noOfPeriods = gsc.periods().size(); + + var sum = 0.; + for (var period = 0; period < noOfPeriods; period++) { + sum += simulatePeriod(osc, gt, period, bestScheduleCollector); } - return result.build(); + return sum; } /** * Calculates the cost of one Period under the given Schedule. * - * @param p the {@link Params} - * @param op the current {@link OptimizePeriod} - * @param state the {@link StateMachine} of the current period - * @param nextEssInitial the initial SoC-Energy; also used as return value - * @param collect a {@link Consumer} to collect the simulation results if - * required. We are not always collecting results to - * reduce workload during simulation. - * @return the cost, lower is better; always positive + * @param simulation the {@link OneSimulationContext} + * @param gt the simulated {@link Genotype} + * @param periodIndex the index of the simulated period + * @param bestScheduleCollector the {@link BestScheduleCollector}; or null + * @return the cost, lower is better, always positive; + * {@link Double#POSITIVE_INFINITY} on error */ - protected static double simulatePeriod(Params p, OptimizePeriod op, StateMachine state, - final AtomicInteger nextEssInitial, Consumer collect) { - // Constants - final var essInitial = max(0, nextEssInitial.get()); // always at least '0' + public static double simulatePeriod(OneSimulationContext simulation, Genotype gt, int periodIndex, + BestScheduleCollector bestScheduleCollector) { + final var period = simulation.global.periods().get(periodIndex); + final var handlers = simulation.global.handlers(); + final var model = EnergyFlow.Model.from(simulation, period); - // Calculate Energy-Flow - final var ef = switch (state) { - case BALANCING -> EnergyFlow.withBalancing(p, op, essInitial); - case DELAY_DISCHARGE -> EnergyFlow.withDelayDischarge(p, op, essInitial); - case CHARGE_GRID -> EnergyFlow.withChargeGrid(p, op, essInitial); - }; + var eshIndex = 0; + for (var esh : handlers) { + if (esh instanceof EnergyScheduleHandler.WithDifferentStates e) { + // Simulate with state given by Genotype + e.simulatePeriod(simulation, period, model, gt.get(eshIndex++).get(periodIndex).intValue()); + } else if (esh instanceof EnergyScheduleHandler.WithOnlyOneState e) { + e.simulatePeriod(simulation, period, model); + } + } - nextEssInitial.set(essInitial - ef.ess()); + final EnergyFlow energyFlow = model.solve(); + + if (energyFlow == null) { + LOG.error("Error while simulating period [" + periodIndex + "]"); + // TODO add configurable debug logging + // LOG.info(simulation.toString()); + // model.logConstraints(); + // model.logMinMaxValues(); + return Double.POSITIVE_INFINITY; + } // Calculate Cost + // TODO should be done also by ESH to enable this use-case: + // https://community.openems.io/t/limitierung-bei-negativen-preisen-und-lastgang-einkauf/2713/2 double cost; - if (ef.grid() > 0) { + if (energyFlow.getGrid() > 0) { // Filter negative prices - var price = max(0, op.price()); + var price = Math.max(0, period.price()); cost = // Cost for direct Consumption - ef.gridToConsumption() * price + energyFlow.getGridToCons() * price // Cost for future Consumption after storage - + ef.gridToEss() * price * EFFICIENCY_FACTOR; + + energyFlow.getGridToEss() * price * EFFICIENCY_FACTOR; } else { // Sell-to-Grid cost = 0.; } - if (collect != null) { - var postprocessedState = postprocessSimulatorState(state, // - EnergyFlow.withBalancing(p, op, essInitial), // - EnergyFlow.withDelayDischarge(p, op, essInitial), // - EnergyFlow.withChargeGrid(p, op, essInitial)); - collect.accept(new Period(op, postprocessedState, essInitial, ef)); + if (bestScheduleCollector != null) { + final var srp = SimulationResult.Period.from(period, energyFlow, simulation.getEssInitial()); + bestScheduleCollector.allPeriods.accept(srp); + eshIndex = 0; + for (var esh : handlers) { + if (esh instanceof EnergyScheduleHandler.WithDifferentStates e) { + bestScheduleCollector.eshStates.accept(new EshToState(e, srp, // + e.postProcessPeriod(period, simulation, energyFlow, + gt.get(eshIndex++).get(periodIndex).intValue()))); + } + } } + + // Prepare for next period + simulation.calculateEssInitial(energyFlow.getEss()); + return cost; } /** - * Runs the optimization with default settings. + * Runs the optimization and returns the "best" simulation result. * - * @param p the {@link Params} - * @param executionLimitSeconds limit.byExecutionTime.ofSeconds - * @return the best schedule + * @param previousResult the {@link SimulationResult} of the + * previous optimization run + * @param engineInterceptor an interceptor for the + * {@link Engine.Builder} + * @param evolutionStreamInterceptor an interceptor for the + * {@link EvolutionStream} + * @return the best Schedule */ - protected static StateMachine[] getBestSchedule(Params p, long executionLimitSeconds) { - return getBestSchedule(p, executionLimitSeconds, null, null); - } + public SimulationResult getBestSchedule(SimulationResult previousResult, + Function, Engine.Builder> engineInterceptor, + Function, EvolutionStream> evolutionStreamInterceptor) { + // Genotype: + // - Separate IntegerChromosome per EnergyScheduleHandler WithDifferentStates + // - Chromosome length = number of periods + // - Integer-Genes represent the state + final var chromosomes = this.gsc.handlers().stream() // + .filter(EnergyScheduleHandler.WithDifferentStates.class::isInstance) // + .map(EnergyScheduleHandler.WithDifferentStates.class::cast) // + .map(esh -> IntegerChromosome.of(0, esh.getAvailableStates().length, this.gsc.periods().size())) // + .toList(); + if (chromosomes.isEmpty()) { + return SimulationResult.EMPTY; + } + final var gtf = Genotype.of(chromosomes); - protected static StateMachine[] getBestSchedule(Params p, long executionLimitSeconds, Integer populationSize, - Integer limit) { - // Return pure BALANCING Schedule if no predictions are available - if (!paramsAreValid(p)) { - return p.optimizePeriods().stream() // - .map(op -> StateMachine.BALANCING) // - .toArray(StateMachine[]::new); + // Decide for single- or multi-threading + final Executor executor; + final var availableCores = Runtime.getRuntime().availableProcessors() - 1; + if (availableCores > 1) { + // Executor is a Thread-Pool with CPU-Cores minus one + executor = new ForkJoinPool(availableCores); + System.out.println("OPTIMIZER Executor runs on " + availableCores + " cores"); + } else { + // Executor is the current thread + executor = Runnable::run; + System.out.println("OPTIMIZER Executor runs on current thread"); } - var gtf = Genotype.of(IntegerChromosome.of(IntegerGene.of(0, p.states().length)), p.optimizePeriods().size()); // - var eval = (Function, Double>) (gt) -> { - var modes = new StateMachine[p.optimizePeriods().size()]; - for (var i = 0; i < modes.length; i++) { - modes[i] = p.states()[gt.get(i).get(0).intValue()]; - } - return calculateCost(p, modes); - }; + // Build the Jenetics Engine var engine = Engine // - .builder(eval, gtf) // - .executor(Runnable::run) // current thread + .builder(this.cache::getUnchecked, gtf) // + .executor(executor) // .minimizing(); - if (populationSize != null) { - engine.populationSize(populationSize); // + if (engineInterceptor != null) { + engine = engineInterceptor.apply(engine); + } + + // Start with previous simulation result as initial population if available + var initialPopulation = fromExistingSimulationResult(this.gsc, previousResult); + EvolutionStream stream; + if (previousResult != null) { + stream = engine.build().stream(List.of(initialPopulation)); + } else { + stream = engine.build().stream(); } - Stream> stream = engine.build() // - .stream(buildInitialPopulation(p)) // - .limit(byExecutionTime(ofSeconds(executionLimitSeconds))); // - if (limit != null) { - stream = stream.limit(limit); // apply optional limit + stream = stream.limit(result -> !currentThread().isInterrupted()); + if (evolutionStreamInterceptor != null) { + stream = evolutionStreamInterceptor.apply(stream); } + + // Start the evaluation var bestGt = stream // .collect(toBestGenotype()); - return IntStream.range(0, p.optimizePeriods().size()) // - .mapToObj(period -> p.states()[bestGt.get(period).get(0).intValue()]) // - .toArray(StateMachine[]::new); + + return SimulationResult.fromQuarters(this.gsc, bestGt); + } + + protected static record BestScheduleCollector(// + Consumer allPeriods, // + Consumer eshStates) { + } + + protected static record EshToState(// + EnergyScheduleHandler.WithDifferentStates esh, // + SimulationResult.Period period, // + int postProcessedStateIndex) { + } + + /** + * Builds a log string of this {@link Simulator}. + * + * @param prefix a line prefix + * @return log string + */ + public String toLogString(String prefix) { + return prefix + toStringHelper(this) // + .addValue(this.gsc) // + .addValue(this.cache.stats()) // + .toString(); } } diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Utils.java b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Utils.java index 4bc3d97858c..e703ab1e0c6 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Utils.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Utils.java @@ -1,89 +1,44 @@ package io.openems.edge.energy.optimizer; -import static com.google.common.collect.Streams.concat; import static io.openems.common.utils.DateUtils.roundDownToQuarter; -import static io.openems.edge.common.type.TypeUtils.multiply; -import static io.openems.edge.common.type.TypeUtils.orElse; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.CHARGE_GRID; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; -import static io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffController.PERIODS_PER_HOUR; -import static java.lang.Math.max; -import static java.lang.Math.round; -import static java.util.Arrays.stream; +import static java.lang.Thread.sleep; import java.time.Clock; import java.time.Duration; import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Objects; -import java.util.Set; -import java.util.TreeMap; -import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.random.RandomGeneratorFactory; -import java.util.stream.IntStream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSortedMap; -import com.google.common.collect.Streams; +import com.google.common.collect.Ordering; import io.jenetics.util.RandomRegistry; -import io.openems.common.exceptions.InvalidValueException; -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.timedata.Resolution; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.function.ThrowingSupplier; import io.openems.common.types.ChannelAddress; -import io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve; -import io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischarge; -import io.openems.edge.controller.ess.timeofusetariff.StateMachine; -import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffController; -import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl; -import io.openems.edge.energy.jsonrpc.GetScheduleResponse; -import io.openems.edge.energy.optimizer.ScheduleDatas.ScheduleData; -import io.openems.edge.energy.optimizer.Simulator.Period; -import io.openems.edge.ess.api.SymmetricEss; -import io.openems.edge.timedata.api.Timedata; -import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; +import io.openems.edge.energy.api.EnergySchedulable; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; +import io.openems.edge.scheduler.api.Scheduler; -/** - * Utils for {@link TimeOfUseTariffController}. - * - *

- * All energy values are in [Wh] and positive, unless stated differently. - */ public final class Utils { private Utils() { } - /** Keep some buffer to avoid scheduling errors because of bad predictions. */ - public static final float ESS_MAX_SOC = 90F; - /** Limit Charge Power for §14a EnWG. */ public static final int ESS_LIMIT_14A_ENWG = -4200; - /** - * C-Rate (capacity divided by time) during {@link StateMachine#CHARGE_GRID}. - * With a C-Rate of 0.5 the battery gets fully charged within 2 hours. - */ - public static final float ESS_CHARGE_C_RATE = 0.5F; - - public static final ChannelAddress SUM_PRODUCTION = new ChannelAddress("_sum", "ProductionActivePower"); - public static final ChannelAddress SUM_CONSUMPTION = new ChannelAddress("_sum", "ConsumptionActivePower"); public static final ChannelAddress SUM_GRID = new ChannelAddress("_sum", "GridActivePower"); - public static final ChannelAddress SUM_UNMANAGED_CONSUMPTION = new ChannelAddress("_sum", - "UnmanagedConsumptionActivePower"); public static final ChannelAddress SUM_ESS_DISCHARGE_POWER = new ChannelAddress("_sum", "EssDischargePower"); public static final ChannelAddress SUM_ESS_SOC = new ChannelAddress("_sum", "EssSoc"); protected static final long EXECUTION_LIMIT_SECONDS_BUFFER = 30; protected static final long EXECUTION_LIMIT_SECONDS_MINIMUM = 60; - private static final Logger LOG = LoggerFactory.getLogger(Utils.class); - /** * Initializes the Jenetics {@link RandomRegistry} for production. */ @@ -121,308 +76,53 @@ private static void initializeRandomRegistry(boolean isUnitTest) { } /** - * Create {@link Params} for {@link Simulator}. + * Creates a {@link Simulator}. * - * @param globalContext the {@link GlobalContext} object - * @param existingSchedule the existing schedule, i.e. result of previous - * optimization - * @return {@link Params} - * @throws InvalidValueException on error - */ - public static Params createSimulatorParams(GlobalContext globalContext, - ImmutableSortedMap existingSchedule) throws InvalidValueException { - final var time = roundDownToQuarter(ZonedDateTime.now()); - - // Prediction values - final var predictionConsumption = joinConsumptionPredictions(4, // - globalContext.predictorManager().getPrediction(SUM_CONSUMPTION).asArray(), // - globalContext.predictorManager().getPrediction(SUM_UNMANAGED_CONSUMPTION).asArray()); - final var predictionProduction = generateProductionPrediction(// - globalContext.predictorManager().getPrediction(SUM_PRODUCTION).asArray(), // - predictionConsumption.length); - - // Prices contains the price values and the time it is retrieved. - final var prices = globalContext.timeOfUseTariff().getPrices(); - - // Ess information. - TimeOfUseTariffControllerImpl.Context context = globalContext.energyScheduleHandler().getContext(); - final var essTotalEnergy = context.ess().getCapacity().getOrError(); - final var essMinSocEnergy = getEssMinSocEnergy(context, essTotalEnergy); - final var essMaxSocEnergy = round(ESS_MAX_SOC / 100F * essTotalEnergy); - final var essSoc = context.ess().getSoc().getOrError(); - final var essSocEnergy = essTotalEnergy /* [Wh] */ / 100 * essSoc; - - // Power Values for scheduling battery for individual periods. - var maxDischargePower = globalContext.sum().getEssMaxDischargePower().orElse(1000 /* at least 1000 */); - var maxChargePower = globalContext.sum().getEssMaxDischargePower().orElse(-1000 /* at least 1000 */); - if (context.limitChargePowerFor14aEnWG()) { - maxChargePower = max(ESS_LIMIT_14A_ENWG, maxChargePower); // Apply §14a EnWG limit - } - - return Params.create() // - .setTime(time) // - .setEssTotalEnergy(essTotalEnergy) // - .setEssMinSocEnergy(essMinSocEnergy) // - .setEssMaxSocEnergy(essMaxSocEnergy) // - .setEssInitialEnergy(essSocEnergy) // - .setEssMaxChargeEnergy(toEnergy(Math.abs(maxChargePower))) // - .setEssMaxDischargeEnergy(toEnergy(maxDischargePower)) // - .seMaxBuyFromGrid(toEnergy(context.maxChargePowerFromGrid())) // - .setProductions(stream(interpolateArray(predictionProduction)).map(v -> toEnergy(v)).toArray()) // - .setConsumptions(stream(interpolateArray(predictionConsumption)).map(v -> toEnergy(v)).toArray()) // - .setPrices(interpolateArray(prices.asArray())) // - .setStates(context.controlMode().states) // - .setExistingSchedule(existingSchedule) // - .build(); - } - - /** - * Postprocesses production prediction; makes sure length is at least the same - * as consumption prediction - filling up with zeroes. - * - * @param prediction the production prediction - * @param minLength the min length (= consumption prediction length) - * @return new production prediction - */ - protected static Integer[] generateProductionPrediction(Integer[] prediction, int minLength) { - if (prediction.length >= minLength) { - return prediction; - } - return IntStream.range(0, minLength) // - .mapToObj(i -> i > prediction.length - 1 ? 0 : prediction[i]) // - .toArray(Integer[]::new); - } - - protected static Integer[] joinConsumptionPredictions(int splitAfterIndex, Integer[] totalConsumption, - Integer[] unmanagedConsumption) { - return Streams.concat(// - stream(totalConsumption) // - .limit(splitAfterIndex), // - stream(unmanagedConsumption) // - .skip(splitAfterIndex)) // - .toArray(Integer[]::new); - } - - protected static boolean paramsAreValid(Params p) { - if (p.optimizePeriods().isEmpty()) { - // No periods are available - LOG.warn("No periods are available"); - return false; - } - if (p.optimizePeriods().stream() // - .allMatch(pp -> pp.production() == 0 && pp.consumption() == 0)) { - // Production and Consumption predictions are all zero - LOG.warn("Production and Consumption predictions are all zero"); - return false; - } - if (p.optimizePeriods().stream() // - .mapToDouble(Params.OptimizePeriod::price) // - .distinct() // - .count() <= 1) { - // Prices are all the same - LOG.info("Prices are all the same"); - return false; - } - - return true; - } - - /** - * Returns the amount of energy that is not available for scheduling because of - * a configured minimum SoC. - * - * @param context the {@link TimeOfUseTariffControllerImpl.Context} - * @param essCapacity net {@link SymmetricEss.ChannelId#CAPACITY} - * @return the value in [Wh] - */ - protected static int getEssMinSocEnergy(TimeOfUseTariffControllerImpl.Context context, int essCapacity) { - return essCapacity /* [Wh] */ / 100 // - * getEssMinSocPercentage(// - context.ctrlLimitTotalDischarges(), // - context.ctrlEmergencyCapacityReserves()); - } + *

+ * This will possibly run forever and call the callbacks multiple times before + * returning. + * + * @param gscSupplier a {@link Supplier} for {@link GlobalSimulationsContext} + * @param interruptFlag a flag to interrupt the threads + * @param simulator a callback for a {@link Simulator}; possibly null + * @param error a callback for a error string + */ + public static synchronized void createSimulator( + ThrowingSupplier gscSupplier, AtomicBoolean interruptFlag, + Consumer simulator, Consumer> error) { + while (!interruptFlag.get()) { + try { + simulator.accept(new Simulator(gscSupplier.get())); + return; - /** - * Returns the configured minimum SoC, or zero. - * - * @param ctrlLimitTotalDischarges the list of - * {@link ControllerEssLimitTotalDischarge} - * @param ctrlEmergencyCapacityReserves the list of - * {@link ControllerEssEmergencyCapacityReserve} - * @return the value in [%] - */ - public static int getEssMinSocPercentage(List ctrlLimitTotalDischarges, - List ctrlEmergencyCapacityReserves) { - return concat(// - ctrlLimitTotalDischarges.stream() // - .map(ctrl -> ctrl.getMinSoc().get()) // - .filter(Objects::nonNull) // - .mapToInt(v -> max(0, v)), // only positives - ctrlEmergencyCapacityReserves.stream() // - .map(ctrl -> ctrl.getActualReserveSoc().get()) // - .filter(Objects::nonNull) // - .mapToInt(v -> max(0, v))) // only positives - .max().orElse(0); - } + } catch (OpenemsException | IllegalArgumentException e) { + e.printStackTrace(); - /** - * Interpolate an Array of {@link Double}s. - * - *

- * Replaces nulls with previous value. If first entry is null, it is set to - * first available value. If all values are null, all are set to 0. - * - * @param values the values - * @return values without nulls - */ - protected static double[] interpolateArray(Double[] values) { - var firstNonNull = stream(values) // - .filter(Objects::nonNull) // - .findFirst(); - var lastNonNullIndex = IntStream.range(0, values.length) // - .filter(i -> values[i] != null) // - .reduce((first, second) -> second); - if (lastNonNullIndex.isEmpty()) { - return new double[0]; - } - var result = new double[lastNonNullIndex.getAsInt() + 1]; - if (firstNonNull.isEmpty()) { - // all null - return result; - } - double last = firstNonNull.get(); - for (var i = 0; i < result.length; i++) { - double value = orElse(values[i], last); - result[i] = last = value; - } - return result; - } + simulator.accept(null); + error.accept(() -> "Stuck trying to get GlobalSimulationsContext. " + e.getMessage()); - /** - * Interpolate an Array of {@link Integer}s. - * - *

- * Replaces nulls with previous value. If first entry is null, it is set to - * first available value. If all values are null, all are set to 0. - * - * @param values the values - * @return values without nulls - */ - protected static int[] interpolateArray(Integer[] values) { - var firstNonNull = stream(values) // - .filter(Objects::nonNull) // - .findFirst(); - var lastNonNullIndex = IntStream.range(0, values.length) // - .filter(i -> values[i] != null) // - .reduce((first, second) -> second); // - if (lastNonNullIndex.isEmpty()) { - return new int[0]; - } - var result = new int[lastNonNullIndex.getAsInt() + 1]; - if (firstNonNull.isEmpty()) { - // all null - return result; - } - int last = firstNonNull.get(); - for (var i = 0; i < result.length; i++) { - int value = orElse(values[i], last); - result[i] = last = value; - } - return result; - } + try { + sleep(10_000); + } catch (InterruptedException e1) { + e.printStackTrace(); - protected static int findFirstPeakIndex(int fromIndex, double[] values) { - if (values.length <= fromIndex) { - return fromIndex; - } else { - var previous = values[fromIndex]; - for (var i = fromIndex + 1; i < values.length; i++) { - var value = values[i]; - if (value < previous) { - return i - 1; + simulator.accept(null); + error.accept(() -> "Unable to create global simulations context: " + e1.getMessage()); } - previous = value; } } - return values.length - 1; - } - protected static int findFirstValleyIndex(int fromIndex, double[] values) { - if (values.length <= fromIndex) { - return fromIndex; - } else { - var previous = values[fromIndex]; - for (var i = fromIndex + 1; i < values.length; i++) { - var value = values[i]; - if (value > previous) { - return i - 1; - } - previous = value; - } - } - return values.length - 1; + simulator.accept(null); + error.accept(() -> "Unable to create global simulations context -> abort"); } /** - * Utilizes the previous three hours' data and computes the next 21 hours data - * from the {@link Optimizer} provided, then concatenates them to generate a - * 24-hour {@link GetScheduleResponse}. + * Calculates the ExecutionLimitSeconds for the {@link Optimizer}. * - * @param optimizer the {@link Optimizer} - * @param requestId the JSON-RPC request-id - * @param timedata the{@link Timedata} - * @param timeOfUseTariff the {@link TimeOfUseTariff} - * @param componentId the Component-ID - * @param now the current {@link ZonedDateTime} (will get rounded - * down to 15 minutes) - * @return the {@link GetScheduleResponse} - * @throws OpenemsNamedException on error + * @return execution limit in [s] */ - public static GetScheduleResponse handleGetScheduleRequest(Optimizer optimizer, UUID requestId, Timedata timedata, - TimeOfUseTariff timeOfUseTariff, String componentId, ZonedDateTime now) { - final var b = ImmutableList.builder(); - now = roundDownToQuarter(now); - final var fromTime = now.minusHours(3); - - final var params = optimizer.getParams(); - if (params != null) { - // Process last three hours of historic data - final var channelQuarterlyPrices = new ChannelAddress(componentId, "QuarterlyPrices"); - final var channelStateMachine = new ChannelAddress(componentId, "StateMachine"); - try { - var queryResult = timedata.queryHistoricData(null, fromTime, now, // - Set.of(channelQuarterlyPrices, channelStateMachine, // - SUM_GRID, SUM_PRODUCTION, SUM_CONSUMPTION, SUM_ESS_DISCHARGE_POWER, SUM_ESS_SOC), - new Resolution(15, ChronoUnit.MINUTES)); - ScheduleData.fromHistoricDataQuery(// - params.essTotalEnergy(), channelQuarterlyPrices, channelStateMachine, queryResult) // - .forEach(b::add); - } catch (Exception e) { - LOG.warn("Unable to read historic data: " + e.getMessage()); - } - } - - // Process future schedule - final var schedule = optimizer.getSchedule(); - optimizer.getSchedule().values().stream() // - .flatMap(ScheduleData::fromPeriod) // - .forEach(b::add); - - // Find 'toTime' of result - final ZonedDateTime toTime; - if (!schedule.isEmpty()) { - toTime = schedule.lastKey(); - } else { - var pricesPerQuarter = timeOfUseTariff.getPrices().pricePerQuarter; - if (!pricesPerQuarter.isEmpty()) { - toTime = pricesPerQuarter.lastKey(); - } else { - toTime = fromTime; - } - } - - return new GetScheduleResponse(requestId, fromTime, toTime, - new ScheduleDatas(params.essTotalEnergy(), b.build())); + public static long calculateExecutionLimitSeconds() { + return calculateExecutionLimitSeconds(Clock.systemDefaultZone()); } /** @@ -442,64 +142,6 @@ public static long calculateExecutionLimitSeconds(Clock clock) { return Duration.between(now, nextQuarter.plusMinutes(15)).getSeconds(); } - /** - * Post-Process a state of a Period during Simulation, i.e. replace with - * 'better' state with the same behaviour. - * - *

- * NOTE: heavy computation is ok here, because this method is called only at the - * end with the best Schedule. - * - * @param state the initial state - * @param efBalancing the {@link EnergyFlow} as it would be in - * {@link StateMachine#BALANCING} - * @param efDelayDischarge the {@link EnergyFlow} as it would be in - * {@link StateMachine#DELAY_DISCHARGE} - * @param efChargeGrid the {@link EnergyFlow} as it would be in - * {@link StateMachine#CHARGE_GRID} - * @return the new state - */ - public static StateMachine postprocessSimulatorState(StateMachine state, EnergyFlow efBalancing, - EnergyFlow efDelayDischarge, EnergyFlow efChargeGrid) { - if (state == CHARGE_GRID) { - // CHARGE_GRID,... - if (efChargeGrid.ess() >= efDelayDischarge.ess()) { - // but battery charge/discharge is the same as DELAY_DISCHARGE - state = DELAY_DISCHARGE; - } - } - - if (state == DELAY_DISCHARGE) { - // DELAY_DISCHARGE,... - if (efDelayDischarge.ess() >= efBalancing.ess()) { - // but battery charge/discharge is the same as BALANCING - state = BALANCING; - } - } - - return state; - } - - /** - * Converts power [W] to energy [Wh/15 min]. - * - * @param power the power value - * @return the energy value - */ - public static int toEnergy(int power) { - return power / PERIODS_PER_HOUR; - } - - /** - * Converts energy [Wh/15 min] to power [W]. - * - * @param energy the energy value - * @return the power value - */ - public static Integer toPower(Integer energy) { - return multiply(energy, PERIODS_PER_HOUR); - } - /** * Prints the Schedule to System.out. * @@ -507,36 +149,45 @@ public static Integer toPower(Integer energy) { * NOTE: The output format is suitable as input for "RunOptimizerFromLogApp". * This is useful to re-run a simulation. * - * @param params the {@link Params} - * @param periods the map of {@link Period}s + * @param simulator the {@link Simulator} + * @param simulationResult the {@link SimulationResult} */ - protected static void logSchedule(Params params, ImmutableSortedMap periods) { - System.out.println("OPTIMIZER " + params.toLogString()); - System.out.println(ScheduleDatas.fromSchedule(params.essTotalEnergy(), periods).toLogString("OPTIMIZER ")); + public static void logSimulationResult(Simulator simulator, SimulationResult simulationResult) { + final var prefix = "OPTIMIZER "; + System.out.println(simulator.toLogString(prefix)); + System.out.println(simulationResult.toLogString(prefix)); } /** - * Updates the active Schedule with a new Schedule. - * - *

- *

    - *
  • Period of the currently active Quarter is never changed - *
  • Old Periods are removed from the Schedule - *
  • Remaining Schedules are updated from new Schedule - *
+ * Sorts the list of {@link EnergySchedulable}s by the order given by + * {@link Scheduler}. * - * @param now the current {@link ZonedDateTime} - * @param schedule the active Schedule - * @param newSchedule the new Schedule + * @param scheduler the {@link Scheduler} + * @param list the list of {@link EnergySchedulable}s + * @return sorted list of {@link EnergySchedulable}s */ - public static void updateSchedule(ZonedDateTime now, TreeMap schedule, - ImmutableSortedMap newSchedule) { - var thisQuarter = roundDownToQuarter(now); - var current = schedule.get(thisQuarter); - schedule.clear(); - schedule.putAll(newSchedule); - if (current != null) { - schedule.put(thisQuarter, current); - } + public static ImmutableList sortByScheduler(Scheduler scheduler, List list) { + var ref = scheduler.getControllers().stream().toList(); + + final Ordering byScheduler = new Ordering() { + public int compare(String left, String right) { + var leftIdx = ref.indexOf(left); + var rightIdx = ref.indexOf(right); + if (leftIdx < 0 && rightIdx < 0) { // both not found + return Objects.compare(left, right, String::compareTo); + } else if (leftIdx < 0) { // only right is in list + return 1; + } else if (rightIdx < 0) { // only left is in list + return -1; + } else { + return leftIdx - rightIdx; + } + } + }; + + return byScheduler // + .onResultOf(EnergySchedulable::id) // + .immutableSortedCopy(list); } + } diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/jsonrpc/GetScheduleResponse.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/jsonrpc/GetScheduleResponse.java similarity index 90% rename from io.openems.edge.energy/src/io/openems/edge/energy/jsonrpc/GetScheduleResponse.java rename to io.openems.edge.energy/src/io/openems/edge/energy/v1/jsonrpc/GetScheduleResponse.java index e27461abc45..e20510d8fe3 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/jsonrpc/GetScheduleResponse.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/jsonrpc/GetScheduleResponse.java @@ -1,4 +1,4 @@ -package io.openems.edge.energy.jsonrpc; +package io.openems.edge.energy.v1.jsonrpc; import java.time.ZonedDateTime; import java.util.Map.Entry; @@ -9,8 +9,8 @@ import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.common.utils.JsonUtils; -import io.openems.edge.energy.optimizer.ScheduleDatas; -import io.openems.edge.energy.optimizer.ScheduleDatas.ScheduleData; +import io.openems.edge.energy.v1.optimizer.ScheduleDatas; +import io.openems.edge.energy.v1.optimizer.ScheduleDatas.ScheduleData; /** * Represents a JSON-RPC Response for 'getMeters'. @@ -34,6 +34,7 @@ * } * */ +@Deprecated public class GetScheduleResponse extends JsonrpcResponseSuccess { private final ZonedDateTime fromDate; diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/EnergyFlow.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/EnergyFlowV1.java similarity index 76% rename from io.openems.edge.energy/src/io/openems/edge/energy/optimizer/EnergyFlow.java rename to io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/EnergyFlowV1.java index bf108379c74..3de095d0913 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/EnergyFlow.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/EnergyFlowV1.java @@ -1,16 +1,17 @@ -package io.openems.edge.energy.optimizer; +package io.openems.edge.energy.v1.optimizer; import static io.openems.edge.common.type.TypeUtils.fitWithin; import static java.lang.Math.max; import static java.lang.Math.min; import io.openems.edge.controller.ess.timeofusetariff.StateMachine; -import io.openems.edge.energy.optimizer.Params.OptimizePeriod; +import io.openems.edge.energy.v1.optimizer.ParamsV1.OptimizePeriod; /** * Simulates a detailed Energy-Flow. */ -public record EnergyFlow(// +@Deprecated +public record EnergyFlowV1(// int production, /* positive */ int consumption, /* positive */ int ess, /* charge negative, discharge positive */ @@ -24,49 +25,49 @@ public record EnergyFlow(// ) { /** - * Simulate {@link EnergyFlow} in {@link StateMachine#BALANCING}. + * Simulate {@link EnergyFlowV1} in {@link StateMachine#BALANCING}. * - * @param p the {@link Params} + * @param p the {@link ParamsV1} * @param op the {@link OptimizePeriod} * @param essInitial ESS Initially Available Energy (SoC in [Wh]) - * @return the {@link EnergyFlow} + * @return the {@link EnergyFlowV1} */ - public static EnergyFlow withBalancing(Params p, OptimizePeriod op, int essInitial) { + public static EnergyFlowV1 withBalancing(ParamsV1 p, OptimizePeriod op, int essInitial) { return create(p, op, essInitial, // p.essTotalEnergy(), // Allow Balancing till full battery op.consumption() - op.production()); } /** - * Simulate {@link EnergyFlow} in {@link StateMachine#DELAY_DISCHARGE}. + * Simulate {@link EnergyFlowV1} in {@link StateMachine#DELAY_DISCHARGE}. * - * @param p the {@link Params} + * @param p the {@link ParamsV1} * @param op the {@link OptimizePeriod} * @param essInitial ESS Initially Available Energy (SoC in [Wh]) - * @return the {@link EnergyFlow} + * @return the {@link EnergyFlowV1} */ - public static EnergyFlow withDelayDischarge(Params p, OptimizePeriod op, int essInitial) { + public static EnergyFlowV1 withDelayDischarge(ParamsV1 p, OptimizePeriod op, int essInitial) { return create(p, op, essInitial, // p.essTotalEnergy(), // Allow Delay-Discharge with full battery min(0, op.consumption() - op.production())); // Allow charge; no discharge } /** - * Simulate {@link EnergyFlow} in {@link StateMachine#CHARGE_GRID}. + * Simulate {@link EnergyFlowV1} in {@link StateMachine#CHARGE_GRID}. * - * @param p the {@link Params} + * @param p the {@link ParamsV1} * @param op the {@link OptimizePeriod} * @param essInitial ESS Initially Available Energy (SoC in [Wh]) - * @return the {@link EnergyFlow} + * @return the {@link EnergyFlowV1} */ - public static EnergyFlow withChargeGrid(Params p, OptimizePeriod op, int essInitial) { + public static EnergyFlowV1 withChargeGrid(ParamsV1 p, OptimizePeriod op, int essInitial) { return create(p, op, essInitial, // p.essMaxSocEnergy(), // Allow Charge-Grid only till Max-SoC // Same as Delay-Discharge + Charge-From-Grid min(0, op.consumption() - op.production()) - op.essChargeInChargeGrid()); } - protected static EnergyFlow create(Params p, OptimizePeriod op, int essInitial, int essMaxSocEnergy, + protected static EnergyFlowV1 create(ParamsV1 p, OptimizePeriod op, int essInitial, int essMaxSocEnergy, int essTarget) { var essMaxDischarge = max(0, essInitial - p.essMinSocEnergy()); var essMaxCharge = max(0, essMaxSocEnergy - essInitial); @@ -86,7 +87,7 @@ protected static EnergyFlow create(Params p, OptimizePeriod op, int essInitial, var essToConsumption = max(0, min(op.consumption() - productionToConsumption, ess - productionToGrid)); var gridToConsumption = max(0, op.consumption() - essToConsumption - productionToConsumption); var gridToEss = grid - gridToConsumption + productionToGrid; - return new EnergyFlow(// + return new EnergyFlowV1(// op.production(), /* production */ op.consumption(), /* consumption */ ess, /* ess */ diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/GlobalContext.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/GlobalContextV1.java similarity index 67% rename from io.openems.edge.energy/src/io/openems/edge/energy/optimizer/GlobalContext.java rename to io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/GlobalContextV1.java index c7b889a6eff..3af5e879047 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/GlobalContext.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/GlobalContextV1.java @@ -1,25 +1,25 @@ -package io.openems.edge.energy.optimizer; +package io.openems.edge.energy.v1.optimizer; import java.time.Clock; import io.openems.edge.common.sum.Sum; -import io.openems.edge.controller.ess.timeofusetariff.StateMachine; import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffController; -import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl; +import io.openems.edge.controller.ess.timeofusetariff.v1.EnergyScheduleHandlerV1; import io.openems.edge.energy.api.EnergyScheduleHandler; import io.openems.edge.predictor.api.manager.PredictorManager; import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; -public record GlobalContext(// +@Deprecated +public record GlobalContextV1(// Clock clock, // - EnergyScheduleHandler energyScheduleHandler, // + EnergyScheduleHandlerV1 energyScheduleHandler, // Sum sum, // PredictorManager predictorManager, // TimeOfUseTariff timeOfUseTariff) { public static class Builder { private Clock clock; - private EnergyScheduleHandler energyScheduleHandler; + private EnergyScheduleHandlerV1 energyScheduleHandler; private Sum sum; private PredictorManager predictorManager; private TimeOfUseTariff timeOfUseTariff; @@ -41,8 +41,7 @@ public Builder setClock(Clock clock) { * @param energyScheduleHandler the {@link EnergyScheduleHandler} * @return myself */ - public Builder setEnergyScheduleHandler( - EnergyScheduleHandler energyScheduleHandler) { + public Builder setEnergyScheduleHandler(EnergyScheduleHandlerV1 energyScheduleHandler) { this.energyScheduleHandler = energyScheduleHandler; return this; } @@ -81,23 +80,23 @@ public Builder setTimeOfUseTariff(TimeOfUseTariff timeOfUseTariff) { } /** - * Builds the {@link GlobalContext}. + * Builds the {@link GlobalContextV1}. * - * @return the {@link GlobalContext} record + * @return the {@link GlobalContextV1} record */ - public GlobalContext build() { - return new GlobalContext(this.clock, this.energyScheduleHandler, this.sum, this.predictorManager, + public GlobalContextV1 build() { + return new GlobalContextV1(this.clock, this.energyScheduleHandler, this.sum, this.predictorManager, this.timeOfUseTariff); } } /** - * Create a {@link GlobalContext} {@link Builder}. + * Create a {@link GlobalContextV1} {@link Builder}. * * @return a {@link Builder} */ public static Builder create() { - return new GlobalContext.Builder(); + return new GlobalContextV1.Builder(); } } diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/InitialPopulationUtils.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/InitialPopulationV1Utils.java similarity index 91% rename from io.openems.edge.energy/src/io/openems/edge/energy/optimizer/InitialPopulationUtils.java rename to io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/InitialPopulationV1Utils.java index b09e3e3036c..41b99551fcf 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/InitialPopulationUtils.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/InitialPopulationV1Utils.java @@ -1,10 +1,10 @@ -package io.openems.edge.energy.optimizer; +package io.openems.edge.energy.v1.optimizer; import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.CHARGE_GRID; import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; -import static io.openems.edge.energy.optimizer.Utils.findFirstPeakIndex; -import static io.openems.edge.energy.optimizer.Utils.findFirstValleyIndex; +import static io.openems.edge.energy.api.EnergyUtils.findFirstPeakIndex; +import static io.openems.edge.energy.api.EnergyUtils.findFirstValleyIndex; import java.util.Arrays; import java.util.List; @@ -19,11 +19,12 @@ import io.jenetics.IntegerChromosome; import io.jenetics.IntegerGene; import io.openems.edge.controller.ess.timeofusetariff.StateMachine; -import io.openems.edge.energy.optimizer.Params.OptimizePeriod; +import io.openems.edge.energy.v1.optimizer.ParamsV1.OptimizePeriod; -public class InitialPopulationUtils { +@Deprecated +public class InitialPopulationV1Utils { - private InitialPopulationUtils() { + private InitialPopulationV1Utils() { } /** @@ -40,10 +41,10 @@ private InitialPopulationUtils() { * sure, that this one wins in case there are other results with same cost, e.g. * when battery never gets empty anyway. * - * @param p the {@link Params} + * @param p the {@link ParamsV1} * @return the {@link Genotype} */ - public static ImmutableList> buildInitialPopulation(Params p) { + public static ImmutableList> buildInitialPopulation(ParamsV1 p) { var states = List.of(p.states()); if (!states.contains(BALANCING)) { throw new IllegalArgumentException("State option BALANCING is always required!"); diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/OptimizerV1.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/OptimizerV1.java new file mode 100644 index 00000000000..77fda46f75b --- /dev/null +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/OptimizerV1.java @@ -0,0 +1,159 @@ +package io.openems.edge.energy.v1.optimizer; + +import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap; +import static io.openems.edge.energy.optimizer.Utils.calculateExecutionLimitSeconds; +import static io.openems.edge.energy.optimizer.Utils.initializeRandomRegistryForProduction; +import static io.openems.edge.energy.v1.optimizer.SimulatorV1.simulate; +import static io.openems.edge.energy.v1.optimizer.UtilsV1.createSimulatorParams; +import static io.openems.edge.energy.v1.optimizer.UtilsV1.logSchedule; +import static io.openems.edge.energy.v1.optimizer.UtilsV1.updateSchedule; +import static java.lang.Thread.sleep; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableSortedMap; + +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.function.ThrowingSupplier; +import io.openems.common.test.TimeLeapClock; +import io.openems.common.utils.FunctionUtils; +import io.openems.common.worker.AbstractImmediateWorker; +import io.openems.edge.controller.ess.timeofusetariff.v1.EnergyScheduleHandlerV1; +import io.openems.edge.energy.LogVerbosity; +import io.openems.edge.energy.v1.optimizer.SimulatorV1.Period; + +/** + * This task is executed once in the beginning and afterwards every full 15 + * minutes. + */ +@Deprecated +public class OptimizerV1 extends AbstractImmediateWorker { + + private final Logger log = LoggerFactory.getLogger(OptimizerV1.class); + + private final Supplier logVerbosity; + private final ThrowingSupplier globalContext; + private final TreeMap schedule = new TreeMap<>(); + + private ParamsV1 params = null; + + public OptimizerV1(Supplier logVerbosity, // + ThrowingSupplier globalContext) { + this.logVerbosity = logVerbosity; + this.globalContext = globalContext; + initializeRandomRegistryForProduction(); + + // Run Optimizer thread in LOW PRIORITY + this.setPriority(Thread.MIN_PRIORITY); + } + + @Override + public void forever() throws InterruptedException, OpenemsException { + this.traceLog(() -> "Start next run of Optimizer"); + + this.createParams(); // this possibly takes forever + + final var globalContext = this.globalContext.get(); + final var start = Instant.now(globalContext.clock()); + + long executionLimitSeconds; + + // Calculate max execution time till next quarter (with buffer) + executionLimitSeconds = calculateExecutionLimitSeconds(globalContext.clock()); + + // Find best Schedule + var schedule = SimulatorV1.getBestSchedule(this.params, executionLimitSeconds); + + // Re-Simulate and keep best Schedule + var newSchedule = simulate(this.params, schedule); + + // Debug Log best Schedule + logSchedule(this.params, newSchedule); + + // Update Schedule from newly simulated Schedule + synchronized (this.schedule) { + updateSchedule(ZonedDateTime.now(globalContext.clock()), this.schedule, newSchedule); + } + + // Send Schedule to Controller + globalContext.energyScheduleHandler().setSchedule(this.schedule.entrySet().stream()// + .collect(toImmutableSortedMap(// + ZonedDateTime::compareTo, // + Entry::getKey, // + e -> new EnergyScheduleHandlerV1.Period<>(e.getValue().state(), + e.getValue().op().essChargeInChargeGrid())))); + + // Sleep remaining time + if (!(globalContext.clock() instanceof TimeLeapClock)) { + var remainingExecutionLimit = Duration + .between(Instant.now(globalContext.clock()), start.plusSeconds(executionLimitSeconds)).getSeconds(); + if (remainingExecutionLimit > 0) { + this.traceLog(() -> "Sleep [" + remainingExecutionLimit + "s] till next run of Optimizer"); + sleep(remainingExecutionLimit * 1000); + } + } + } + + /** + * Try forever till all data is available (e.g. ESS Capacity) + * + * @throws InterruptedException during sleep + */ + private void createParams() throws InterruptedException { + while (true) { + try { + synchronized (this.schedule) { + this.params = createSimulatorParams(this.globalContext.get(), // + this.schedule.entrySet().stream() // + .collect(toImmutableSortedMap(// + ZonedDateTime::compareTo, // + Entry::getKey, e -> e.getValue().state()))); + return; + } + + } catch (OpenemsException e) { + this.traceLog(() -> "Stuck trying to get Params. " + e.getMessage()); + this.params = null; + synchronized (this.schedule) { + this.schedule.clear(); + } + sleep(30_000); + } + } + } + + /** + * Gets the current {@link ParamsV1} or null. + * + * @return the {@link ParamsV1} or null + */ + public ParamsV1 getParams() { + return this.params; + } + + /** + * Gets a copy of the Schedule. + * + * @return {@link ImmutableSortedMap} + */ + public ImmutableSortedMap getSchedule() { + synchronized (this.schedule) { + return ImmutableSortedMap.copyOf(this.schedule); + } + } + + private void traceLog(Supplier message) { + switch (this.logVerbosity.get()) { + case NONE, DEBUG_LOG -> FunctionUtils.doNothing(); + case TRACE -> this.log.info("OPTIMIZER " + message.get()); + } + } +} diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ParamsUtils.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/ParamsUtilsV1.java similarity index 88% rename from io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ParamsUtils.java rename to io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/ParamsUtilsV1.java index cd1e73e75ec..8cca4e3bfdd 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ParamsUtils.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/ParamsUtilsV1.java @@ -1,10 +1,10 @@ -package io.openems.edge.energy.optimizer; +package io.openems.edge.energy.v1.optimizer; import static com.google.common.math.Quantiles.percentiles; -import static io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffController.PERIODS_PER_HOUR; -import static io.openems.edge.energy.optimizer.Utils.ESS_CHARGE_C_RATE; -import static io.openems.edge.energy.optimizer.Utils.findFirstPeakIndex; -import static io.openems.edge.energy.optimizer.Utils.findFirstValleyIndex; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.ESS_CHARGE_C_RATE; +import static io.openems.edge.energy.api.EnergyConstants.PERIODS_PER_HOUR; +import static io.openems.edge.energy.api.EnergyUtils.findFirstPeakIndex; +import static io.openems.edge.energy.api.EnergyUtils.findFirstValleyIndex; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.Math.round; @@ -16,11 +16,12 @@ import com.google.common.primitives.ImmutableIntArray; import io.openems.edge.controller.ess.timeofusetariff.StateMachine; -import io.openems.edge.energy.optimizer.Simulator.Period; +import io.openems.edge.energy.v1.optimizer.SimulatorV1.Period; -public class ParamsUtils { +@Deprecated +public class ParamsUtilsV1 { - private ParamsUtils() { + private ParamsUtilsV1() { } /** diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Params.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/ParamsV1.java similarity index 94% rename from io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Params.java rename to io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/ParamsV1.java index f49fe773eb9..92ce404372a 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Params.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/ParamsV1.java @@ -1,8 +1,8 @@ -package io.openems.edge.energy.optimizer; +package io.openems.edge.energy.v1.optimizer; import static com.google.common.collect.ImmutableList.toImmutableList; -import static io.openems.edge.energy.optimizer.ParamsUtils.calculateChargeEnergyInChargeGrid; -import static io.openems.edge.energy.optimizer.ParamsUtils.calculatePeriodLengthHourFromIndex; +import static io.openems.edge.energy.v1.optimizer.ParamsUtilsV1.calculateChargeEnergyInChargeGrid; +import static io.openems.edge.energy.v1.optimizer.ParamsUtilsV1.calculatePeriodLengthHourFromIndex; import static java.lang.Math.min; import java.time.ZonedDateTime; @@ -16,7 +16,8 @@ import io.openems.edge.controller.ess.timeofusetariff.StateMachine; -public record Params(// +@Deprecated +public record ParamsV1(// /** Start-Timestamp of the Schedule */ ZonedDateTime time, /** ESS Total Energy (Capacity) [Wh] */ @@ -206,15 +207,15 @@ private ImmutableList generatePeriods() { return result.build(); } - public Params build() { - return new Params(this.time, this.essTotalEnergy, this.essMinSocEnergy, this.essMaxSocEnergy, + public ParamsV1 build() { + return new ParamsV1(this.time, this.essTotalEnergy, this.essMinSocEnergy, this.essMaxSocEnergy, this.essInitialEnergy, this.states, // this.existingSchedule, this.generatePeriods()); } } protected static Builder create() { - return new Params.Builder(); + return new ParamsV1.Builder(); } @Override diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ScheduleDatas.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/ScheduleDatas.java similarity index 94% rename from io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ScheduleDatas.java rename to io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/ScheduleDatas.java index 053dac39d13..ed0f95c1600 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ScheduleDatas.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/ScheduleDatas.java @@ -1,4 +1,4 @@ -package io.openems.edge.energy.optimizer; +package io.openems.edge.energy.v1.optimizer; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap; @@ -7,13 +7,13 @@ import static io.openems.common.utils.JsonUtils.getAsDouble; import static io.openems.common.utils.JsonUtils.getAsInt; import static io.openems.common.utils.JsonUtils.toJson; -import static io.openems.edge.energy.optimizer.Utils.SUM_CONSUMPTION; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.SUM_PRODUCTION; import static io.openems.edge.energy.optimizer.Utils.SUM_ESS_DISCHARGE_POWER; import static io.openems.edge.energy.optimizer.Utils.SUM_ESS_SOC; import static io.openems.edge.energy.optimizer.Utils.SUM_GRID; -import static io.openems.edge.energy.optimizer.Utils.SUM_PRODUCTION; -import static io.openems.edge.energy.optimizer.Utils.toEnergy; -import static io.openems.edge.energy.optimizer.Utils.toPower; +import static io.openems.edge.energy.v1.optimizer.UtilsV1.SUM_CONSUMPTION; +import static io.openems.edge.energy.v1.optimizer.UtilsV1.toEnergy; +import static io.openems.edge.energy.v1.optimizer.UtilsV1.toPower; import static java.lang.Double.parseDouble; import static java.lang.Integer.parseInt; import static java.lang.Math.round; @@ -43,24 +43,25 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.types.ChannelAddress; import io.openems.edge.controller.ess.timeofusetariff.StateMachine; -import io.openems.edge.energy.optimizer.Params.Length; -import io.openems.edge.energy.optimizer.Simulator.Period; +import io.openems.edge.energy.v1.optimizer.ParamsV1.Length; +import io.openems.edge.energy.v1.optimizer.SimulatorV1.Period; /** * Data for JSONRPC-Response. Values are in [W]. */ +@Deprecated public record ScheduleDatas(int essTotalEnergy, ImmutableList entries) { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); /** - * Creates {@link ScheduleDatas} from an {@link Optimizer}. + * Creates {@link ScheduleDatas} from an {@link OptimizerV1}. * - * @param optimizer the {@link Optimizer} + * @param optimizer the {@link OptimizerV1} * @return a {@link ScheduleDatas}a * @throws OpenemsException on error */ - public static ScheduleDatas fromSchedule(Optimizer optimizer) throws OpenemsException { + public static ScheduleDatas fromSchedule(OptimizerV1 optimizer) throws OpenemsException { final var schedule = optimizer.getSchedule(); if (schedule == null) { throw new OpenemsException("Has no Schedule"); diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/SimulatorV1.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/SimulatorV1.java new file mode 100644 index 00000000000..52145544f2e --- /dev/null +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/SimulatorV1.java @@ -0,0 +1,178 @@ +package io.openems.edge.energy.v1.optimizer; + +import static io.jenetics.engine.EvolutionResult.toBestGenotype; +import static io.jenetics.engine.Limits.byExecutionTime; +import static io.openems.edge.energy.v1.optimizer.InitialPopulationV1Utils.buildInitialPopulation; +import static io.openems.edge.energy.v1.optimizer.UtilsV1.paramsAreValid; +import static io.openems.edge.energy.v1.optimizer.UtilsV1.postprocessSimulatorState; +import static java.lang.Math.max; +import static java.time.Duration.ofSeconds; + +import java.time.ZonedDateTime; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedMap; + +import io.jenetics.Genotype; +import io.jenetics.IntegerChromosome; +import io.jenetics.IntegerGene; +import io.jenetics.engine.Engine; +import io.jenetics.engine.EvolutionResult; +import io.openems.edge.controller.ess.timeofusetariff.StateMachine; +import io.openems.edge.energy.v1.optimizer.ParamsV1.Length; +import io.openems.edge.energy.v1.optimizer.ParamsV1.OptimizePeriod; + +@Deprecated +public class SimulatorV1 { + + /** Used to incorporate charge/discharge efficiency. */ + public static final double EFFICIENCY_FACTOR = 1.17; + + public record Period(OptimizePeriod op, StateMachine state, int essInitial, EnergyFlowV1 ef) { + } + + /** + * Simulates a Schedule and calculates the cost. + * + * @param p the {@link ParamsV1} + * @param schedule the {@link StateMachine} states of the Schedule + * @return the cost, lower is better; always positive + */ + protected static double calculateCost(ParamsV1 p, StateMachine[] schedule) { + final var nextEssInitial = new AtomicInteger(p.essInitialEnergy()); + var sum = 0.; + for (var i = 0; i < p.optimizePeriods().size(); i++) { + sum += simulatePeriod(p, p.optimizePeriods().get(i), schedule[i], nextEssInitial, null); + } + return sum; + } + + /** + * Simulates a Schedule in quarterly periods. + * + * @param p the {@link ParamsV1} + * @param schedule the {@link StateMachine} states of the Schedule + * @return a Map of {@link Period}s + */ + protected static ImmutableSortedMap simulate(ParamsV1 p, StateMachine[] schedule) { + final var nextEssInitial = new AtomicInteger(p.essInitialEnergy()); + var result = ImmutableSortedMap.naturalOrder(); + for (var i = 0; i < p.optimizePeriods().size(); i++) { + var state = schedule[i]; + var op = p.optimizePeriods().get(i); + var length = op.quarterPeriods().size() == 1 ? Length.QUARTER : Length.HOUR; + // Convert mixed OptimizePeriods to pure quarterly + for (var qp : op.quarterPeriods()) { + var quarterlyOp = new OptimizePeriod(qp.time(), length, qp.essMaxChargeEnergy(), + qp.essMaxDischargeEnergy(), qp.essChargeInChargeGrid(), qp.maxBuyFromGrid(), qp.production(), + qp.consumption(), qp.price(), ImmutableList.of(qp)); + simulatePeriod(p, quarterlyOp, state, nextEssInitial, period -> result.put(period.op().time(), period)); + } + } + return result.build(); + } + + /** + * Calculates the cost of one Period under the given Schedule. + * + * @param p the {@link ParamsV1} + * @param op the current {@link OptimizePeriod} + * @param state the {@link StateMachine} of the current period + * @param nextEssInitial the initial SoC-Energy; also used as return value + * @param collect a {@link Consumer} to collect the simulation results if + * required. We are not always collecting results to + * reduce workload during simulation. + * @return the cost, lower is better; always positive + */ + protected static double simulatePeriod(ParamsV1 p, OptimizePeriod op, StateMachine state, + final AtomicInteger nextEssInitial, Consumer collect) { + // Constants + final var essInitial = max(0, nextEssInitial.get()); // always at least '0' + + // Calculate Energy-Flow + final var ef = switch (state) { + case BALANCING -> EnergyFlowV1.withBalancing(p, op, essInitial); + case DELAY_DISCHARGE -> EnergyFlowV1.withDelayDischarge(p, op, essInitial); + case CHARGE_GRID -> EnergyFlowV1.withChargeGrid(p, op, essInitial); + }; + + nextEssInitial.set(essInitial - ef.ess()); + + // Calculate Cost + double cost; + if (ef.grid() > 0) { + // Filter negative prices + var price = max(0, op.price()); + + cost = // Cost for direct Consumption + ef.gridToConsumption() * price + // Cost for future Consumption after storage + + ef.gridToEss() * price * EFFICIENCY_FACTOR; + + } else { + // Sell-to-Grid + cost = 0.; + } + if (collect != null) { + var postprocessedState = postprocessSimulatorState(state, // + EnergyFlowV1.withBalancing(p, op, essInitial), // + EnergyFlowV1.withDelayDischarge(p, op, essInitial), // + EnergyFlowV1.withChargeGrid(p, op, essInitial)); + collect.accept(new Period(op, postprocessedState, essInitial, ef)); + } + return cost; + } + + /** + * Runs the optimization with default settings. + * + * @param p the {@link ParamsV1} + * @param executionLimitSeconds limit.byExecutionTime.ofSeconds + * @return the best schedule + */ + protected static StateMachine[] getBestSchedule(ParamsV1 p, long executionLimitSeconds) { + return getBestSchedule(p, executionLimitSeconds, null, null); + } + + protected static StateMachine[] getBestSchedule(ParamsV1 p, long executionLimitSeconds, Integer populationSize, + Integer limit) { + // Return pure BALANCING Schedule if no predictions are available + if (!paramsAreValid(p)) { + return p.optimizePeriods().stream() // + .map(op -> StateMachine.BALANCING) // + .toArray(StateMachine[]::new); + } + + var gtf = Genotype.of(IntegerChromosome.of(IntegerGene.of(0, p.states().length)), p.optimizePeriods().size()); // + var eval = (Function, Double>) (gt) -> { + var modes = new StateMachine[p.optimizePeriods().size()]; + for (var i = 0; i < modes.length; i++) { + modes[i] = p.states()[gt.get(i).get(0).intValue()]; + } + return calculateCost(p, modes); + }; + var engine = Engine // + .builder(eval, gtf) // + .executor(Runnable::run) // current thread + .minimizing(); + if (populationSize != null) { + engine.populationSize(populationSize); // + } + Stream> stream = engine.build() // + .stream(buildInitialPopulation(p)) // + .limit(byExecutionTime(ofSeconds(executionLimitSeconds))); // + if (limit != null) { + stream = stream.limit(limit); // apply optional limit + } + var bestGt = stream // + .collect(toBestGenotype()); + return IntStream.range(0, p.optimizePeriods().size()) // + .mapToObj(period -> p.states()[bestGt.get(period).get(0).intValue()]) // + .toArray(StateMachine[]::new); + } +} diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/UtilsV1.java b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/UtilsV1.java new file mode 100644 index 00000000000..d43f90fcff6 --- /dev/null +++ b/io.openems.edge.energy/src/io/openems/edge/energy/v1/optimizer/UtilsV1.java @@ -0,0 +1,382 @@ +package io.openems.edge.energy.v1.optimizer; + +import static io.openems.common.utils.DateUtils.roundDownToQuarter; +import static io.openems.edge.common.type.TypeUtils.multiply; +import static io.openems.edge.common.type.TypeUtils.orElse; +import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; +import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.CHARGE_GRID; +import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; +import static io.openems.edge.controller.ess.timeofusetariff.Utils.ESS_MAX_SOC; +import static io.openems.edge.controller.ess.timeofusetariff.v1.UtilsV1.getEssMinSocPercentage; +import static io.openems.edge.energy.api.EnergyConstants.PERIODS_PER_HOUR; +import static io.openems.edge.energy.api.EnergyUtils.interpolateArray; +import static io.openems.edge.energy.optimizer.Utils.ESS_LIMIT_14A_ENWG; +import static io.openems.edge.energy.optimizer.Utils.SUM_ESS_DISCHARGE_POWER; +import static io.openems.edge.energy.optimizer.Utils.SUM_ESS_SOC; +import static io.openems.edge.energy.optimizer.Utils.SUM_GRID; +import static java.lang.Math.max; +import static java.lang.Math.round; +import static java.util.Arrays.stream; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import java.util.stream.IntStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Streams; + +import io.openems.common.exceptions.InvalidValueException; +import io.openems.common.timedata.Resolution; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.controller.ess.emergencycapacityreserve.statemachine.Context; +import io.openems.edge.controller.ess.timeofusetariff.StateMachine; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffController; +import io.openems.edge.controller.ess.timeofusetariff.v1.EnergyScheduleHandlerV1.ContextV1; +import io.openems.edge.energy.v1.jsonrpc.GetScheduleResponse; +import io.openems.edge.energy.v1.optimizer.ScheduleDatas.ScheduleData; +import io.openems.edge.energy.v1.optimizer.SimulatorV1.Period; +import io.openems.edge.ess.api.SymmetricEss; +import io.openems.edge.timedata.api.Timedata; +import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; + +/** + * Utils for {@link TimeOfUseTariffController}. + * + *

+ * All energy values are in [Wh] and positive, unless stated differently. + */ +@Deprecated +public final class UtilsV1 { + + private UtilsV1() { + } + + public static final ChannelAddress SUM_PRODUCTION = new ChannelAddress("_sum", "ProductionActivePower"); + public static final ChannelAddress SUM_CONSUMPTION = new ChannelAddress("_sum", "ConsumptionActivePower"); + public static final ChannelAddress SUM_UNMANAGED_CONSUMPTION = new ChannelAddress("_sum", + "UnmanagedConsumptionActivePower"); + + private static final Logger LOG = LoggerFactory.getLogger(UtilsV1.class); + + /** + * Create {@link ParamsV1} for {@link SimulatorV1}. + * + * @param globalContext the {@link GlobalContextV1} object + * @param existingSchedule the existing schedule, i.e. result of previous + * optimization + * @return {@link ParamsV1} + * @throws InvalidValueException on error + */ + public static ParamsV1 createSimulatorParams(GlobalContextV1 globalContext, + ImmutableSortedMap existingSchedule) throws InvalidValueException { + final var time = roundDownToQuarter(ZonedDateTime.now()); + + // Prediction values + final var predictionConsumption = joinConsumptionPredictions(4, // + globalContext.predictorManager().getPrediction(SUM_CONSUMPTION).asArray(), // + globalContext.predictorManager().getPrediction(SUM_UNMANAGED_CONSUMPTION).asArray()); + final var predictionProduction = generateProductionPrediction(// + globalContext.predictorManager().getPrediction(SUM_PRODUCTION).asArray(), // + predictionConsumption.length); + + // Prices contains the price values and the time it is retrieved. + final var prices = globalContext.timeOfUseTariff().getPrices(); + + // Ess information. + var context = globalContext.energyScheduleHandler().getContext(); + final var essTotalEnergy = context.ess().getCapacity().getOrError(); + final var essMinSocEnergy = getEssMinSocEnergy(context, essTotalEnergy); + final var essMaxSocEnergy = round(ESS_MAX_SOC / 100F * essTotalEnergy); + final var essSoc = context.ess().getSoc().getOrError(); + final var essSocEnergy = essTotalEnergy /* [Wh] */ / 100 * essSoc; + + // Power Values for scheduling battery for individual periods. + var maxDischargePower = globalContext.sum().getEssMaxDischargePower().orElse(1000 /* at least 1000 */); + var maxChargePower = globalContext.sum().getEssMaxDischargePower().orElse(-1000 /* at least 1000 */); + if (context.limitChargePowerFor14aEnWG()) { + maxChargePower = max(ESS_LIMIT_14A_ENWG, maxChargePower); // Apply §14a EnWG limit + } + + return ParamsV1.create() // + .setTime(time) // + .setEssTotalEnergy(essTotalEnergy) // + .setEssMinSocEnergy(essMinSocEnergy) // + .setEssMaxSocEnergy(essMaxSocEnergy) // + .setEssInitialEnergy(essSocEnergy) // + .setEssMaxChargeEnergy(toEnergy(Math.abs(maxChargePower))) // + .setEssMaxDischargeEnergy(toEnergy(maxDischargePower)) // + .seMaxBuyFromGrid(toEnergy(context.maxChargePowerFromGrid())) // + .setProductions(stream(interpolateArray(predictionProduction)).map(v -> toEnergy(v)).toArray()) // + .setConsumptions(stream(interpolateArray(predictionConsumption)).map(v -> toEnergy(v)).toArray()) // + .setPrices(interpolateDoubleArray(prices.asArray())) // + .setStates(context.controlMode().states) // + .setExistingSchedule(existingSchedule) // + .build(); + } + + /** + * Postprocesses production prediction; makes sure length is at least the same + * as consumption prediction - filling up with zeroes. + * + * @param prediction the production prediction + * @param minLength the min length (= consumption prediction length) + * @return new production prediction + */ + protected static Integer[] generateProductionPrediction(Integer[] prediction, int minLength) { + if (prediction.length >= minLength) { + return prediction; + } + return IntStream.range(0, minLength) // + .mapToObj(i -> i > prediction.length - 1 ? 0 : prediction[i]) // + .toArray(Integer[]::new); + } + + protected static Integer[] joinConsumptionPredictions(int splitAfterIndex, Integer[] totalConsumption, + Integer[] unmanagedConsumption) { + return Streams.concat(// + stream(totalConsumption) // + .limit(splitAfterIndex), // + stream(unmanagedConsumption) // + .skip(splitAfterIndex)) // + .toArray(Integer[]::new); + } + + protected static boolean paramsAreValid(ParamsV1 p) { + if (p.optimizePeriods().isEmpty()) { + // No periods are available + LOG.warn("No periods are available"); + return false; + } + if (p.optimizePeriods().stream() // + .allMatch(pp -> pp.production() == 0 && pp.consumption() == 0)) { + // Production and Consumption predictions are all zero + LOG.warn("Production and Consumption predictions are all zero"); + return false; + } + if (p.optimizePeriods().stream() // + .mapToDouble(ParamsV1.OptimizePeriod::price) // + .distinct() // + .count() <= 1) { + // Prices are all the same + LOG.info("Prices are all the same"); + return false; + } + + return true; + } + + /** + * Returns the amount of energy that is not available for scheduling because of + * a configured minimum SoC. + * + * @param context the {@link Context} + * @param essCapacity net {@link SymmetricEss.ChannelId#CAPACITY} + * @return the value in [Wh] + */ + protected static int getEssMinSocEnergy(ContextV1 context, int essCapacity) { + return essCapacity /* [Wh] */ / 100 // + * getEssMinSocPercentage(// + context.ctrlLimitTotalDischarges(), // + context.ctrlEmergencyCapacityReserves()); + } + + /** + * Interpolate an Array of {@link Double}s. + * + *

+ * Replaces nulls with previous value. If first entry is null, it is set to + * first available value. If all values are null, all are set to 0. + * + * @param values the values + * @return values without nulls + */ + protected static double[] interpolateDoubleArray(Double[] values) { + var firstNonNull = stream(values) // + .filter(Objects::nonNull) // + .findFirst(); + var lastNonNullIndex = IntStream.range(0, values.length) // + .filter(i -> values[i] != null) // + .reduce((first, second) -> second); + if (lastNonNullIndex.isEmpty()) { + return new double[0]; + } + var result = new double[lastNonNullIndex.getAsInt() + 1]; + if (firstNonNull.isEmpty()) { + // all null + return result; + } + double last = firstNonNull.get(); + for (var i = 0; i < result.length; i++) { + double value = orElse(values[i], last); + result[i] = last = value; + } + return result; + } + + /** + * Utilizes the previous three hours' data and computes the next 21 hours data + * from the {@link OptimizerV1} provided, then concatenates them to generate a + * 24-hour {@link GetScheduleResponse}. + * + * @param optimizer the {@link OptimizerV1} + * @param requestId the JSON-RPC request-id + * @param timedata the{@link Timedata} + * @param timeOfUseTariff the {@link TimeOfUseTariff} + * @param componentId the Component-ID + * @param now the current {@link ZonedDateTime} (will get rounded + * down to 15 minutes) + * @return the {@link GetScheduleResponse} + */ + public static GetScheduleResponse handleGetScheduleRequest(OptimizerV1 optimizer, UUID requestId, Timedata timedata, + TimeOfUseTariff timeOfUseTariff, String componentId, ZonedDateTime now) { + final var b = ImmutableList.builder(); + now = roundDownToQuarter(now); + final var fromTime = now.minusHours(3); + + final var params = optimizer.getParams(); + if (params != null) { + // Process last three hours of historic data + final var channelQuarterlyPrices = new ChannelAddress(componentId, "QuarterlyPrices"); + final var channelStateMachine = new ChannelAddress(componentId, "StateMachine"); + try { + var queryResult = timedata.queryHistoricData(null, fromTime, now, // + Set.of(channelQuarterlyPrices, channelStateMachine, // + SUM_GRID, SUM_PRODUCTION, SUM_CONSUMPTION, SUM_ESS_DISCHARGE_POWER, SUM_ESS_SOC), + new Resolution(15, ChronoUnit.MINUTES)); + ScheduleData.fromHistoricDataQuery(// + params.essTotalEnergy(), channelQuarterlyPrices, channelStateMachine, queryResult) // + .forEach(b::add); + } catch (Exception e) { + LOG.warn("Unable to read historic data: " + e.getMessage()); + } + } + + // Process future schedule + final var schedule = optimizer.getSchedule(); + optimizer.getSchedule().values().stream() // + .flatMap(ScheduleData::fromPeriod) // + .forEach(b::add); + + // Find 'toTime' of result + final ZonedDateTime toTime; + if (!schedule.isEmpty()) { + toTime = schedule.lastKey(); + } else { + var pricesPerQuarter = timeOfUseTariff.getPrices().pricePerQuarter; + if (!pricesPerQuarter.isEmpty()) { + toTime = pricesPerQuarter.lastKey(); + } else { + toTime = fromTime; + } + } + + return new GetScheduleResponse(requestId, fromTime, toTime, + new ScheduleDatas(params.essTotalEnergy(), b.build())); + } + + /** + * Post-Process a state of a Period during Simulation, i.e. replace with + * 'better' state with the same behaviour. + * + *

+ * NOTE: heavy computation is ok here, because this method is called only at the + * end with the best Schedule. + * + * @param state the initial state + * @param efBalancing the {@link EnergyFlowV1} as it would be in + * {@link StateMachine#BALANCING} + * @param efDelayDischarge the {@link EnergyFlowV1} as it would be in + * {@link StateMachine#DELAY_DISCHARGE} + * @param efChargeGrid the {@link EnergyFlowV1} as it would be in + * {@link StateMachine#CHARGE_GRID} + * @return the new state + */ + public static StateMachine postprocessSimulatorState(StateMachine state, EnergyFlowV1 efBalancing, + EnergyFlowV1 efDelayDischarge, EnergyFlowV1 efChargeGrid) { + if (state == CHARGE_GRID) { + // CHARGE_GRID,... + if (efChargeGrid.ess() >= efDelayDischarge.ess()) { + // but battery charge/discharge is the same as DELAY_DISCHARGE + state = DELAY_DISCHARGE; + } + } + + if (state == DELAY_DISCHARGE) { + // DELAY_DISCHARGE,... + if (efDelayDischarge.ess() >= efBalancing.ess()) { + // but battery charge/discharge is the same as BALANCING + state = BALANCING; + } + } + + return state; + } + + /** + * Converts power [W] to energy [Wh/15 min]. + * + * @param power the power value + * @return the energy value + */ + public static int toEnergy(int power) { + return power / PERIODS_PER_HOUR; + } + + /** + * Converts energy [Wh/15 min] to power [W]. + * + * @param energy the energy value + * @return the power value + */ + public static Integer toPower(Integer energy) { + return multiply(energy, PERIODS_PER_HOUR); + } + + /** + * Prints the Schedule to System.out. + * + *

+ * NOTE: The output format is suitable as input for "RunOptimizerFromLogApp". + * This is useful to re-run a simulation. + * + * @param params the {@link ParamsV1} + * @param periods the map of {@link Period}s + */ + protected static void logSchedule(ParamsV1 params, ImmutableSortedMap periods) { + System.out.println("OPTIMIZER " + params.toLogString()); + System.out.println(ScheduleDatas.fromSchedule(params.essTotalEnergy(), periods).toLogString("OPTIMIZER ")); + } + + /** + * Updates the active Schedule with a new Schedule. + * + *

+ *

    + *
  • Period of the currently active Quarter is never changed + *
  • Old Periods are removed from the Schedule + *
  • Remaining Schedules are updated from new Schedule + *
+ * + * @param now the current {@link ZonedDateTime} + * @param schedule the active Schedule + * @param newSchedule the new Schedule + */ + public static void updateSchedule(ZonedDateTime now, TreeMap schedule, + ImmutableSortedMap newSchedule) { + var thisQuarter = roundDownToQuarter(now); + var current = schedule.get(thisQuarter); + schedule.clear(); + schedule.putAll(newSchedule); + if (current != null) { + schedule.put(thisQuarter, current); + } + } +} diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/EnergySchedulerImplTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/EnergySchedulerImplTest.java index 10331208f63..bcf2d42086a 100644 --- a/io.openems.edge.energy/test/io/openems/edge/energy/EnergySchedulerImplTest.java +++ b/io.openems.edge.energy/test/io/openems/edge/energy/EnergySchedulerImplTest.java @@ -1,19 +1,23 @@ package io.openems.edge.energy; import static io.openems.common.utils.DateUtils.roundDownToQuarter; -import static io.openems.edge.energy.TestData.CONSUMPTION_PREDICTION_QUARTERLY; -import static io.openems.edge.energy.TestData.HOURLY_PRICES_SUMMER; -import static io.openems.edge.energy.TestData.PRODUCTION_PREDICTION_QUARTERLY; -import static io.openems.edge.energy.optimizer.Utils.SUM_CONSUMPTION; -import static io.openems.edge.energy.optimizer.Utils.SUM_PRODUCTION; +import static io.openems.common.utils.ReflectionUtils.getValueViaReflection; +import static io.openems.edge.energy.LogVerbosity.TRACE; +import static io.openems.edge.energy.api.EnergyConstants.SUM_PRODUCTION; +import static io.openems.edge.energy.api.EnergyConstants.SUM_UNMANAGED_CONSUMPTION; +import static io.openems.edge.energy.api.EnergyUtils.toEnergy; +import static io.openems.edge.energy.api.Version.V2_ENERGY_SCHEDULABLE; +import static io.openems.edge.energy.optimizer.TestData.CONSUMPTION_PREDICTION_QUARTERLY; +import static io.openems.edge.energy.optimizer.TestData.HOURLY_PRICES_SUMMER; +import static io.openems.edge.energy.optimizer.TestData.PRODUCTION_PREDICTION_QUARTERLY; +import static io.openems.edge.ess.power.api.Relationship.GREATER_OR_EQUALS; import static java.time.temporal.ChronoUnit.DAYS; import java.time.Clock; import java.time.Instant; +import java.time.LocalTime; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.List; -import java.util.function.Supplier; import org.junit.Test; @@ -23,12 +27,19 @@ 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.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserveImpl; +import io.openems.edge.controller.ess.fixactivepower.ControllerEssFixActivePowerImpl; +import io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedChargeImpl; +import io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischargeImpl; +import io.openems.edge.controller.ess.timeofusetariff.ControlMode; import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl; -import io.openems.edge.energy.optimizer.GlobalContext; +import io.openems.edge.energy.api.test.DummyEnergySchedulable; import io.openems.edge.energy.optimizer.Optimizer; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.predictor.api.prediction.Prediction; import io.openems.edge.predictor.api.test.DummyPredictor; import io.openems.edge.predictor.api.test.DummyPredictorManager; +import io.openems.edge.scheduler.api.test.DummyScheduler; import io.openems.edge.timedata.test.DummyTimedata; import io.openems.edge.timeofusetariff.test.DummyTimeOfUseTariffProvider; @@ -36,8 +47,6 @@ public class EnergySchedulerImplTest { public static final Clock CLOCK = new TimeLeapClock(Instant.parse("2020-03-04T14:19:00.00Z"), ZoneOffset.UTC); - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { create(CLOCK); @@ -51,33 +60,60 @@ public void test() throws Exception { * @throws Exception on error */ public static EnergySchedulerImpl create(Clock clock) throws Exception { - var now = roundDownToQuarter(ZonedDateTime.now(clock)); + final var now = roundDownToQuarter(ZonedDateTime.now(clock)); final var midnight = now.truncatedTo(DAYS); - var componentManager = new DummyComponentManager(clock); - var sum = new DummySum(); - var predictor0 = new DummyPredictor("predictor0", componentManager, + final var componentManager = new DummyComponentManager(clock); + final var sum = new DummySum() // + .withEssCapacity(10000) // + .withEssSoc(50); + final var ess = new DummyManagedSymmetricEss("ess0"); + final var predictor0 = new DummyPredictor("predictor0", componentManager, Prediction.from(sum, SUM_PRODUCTION, midnight, PRODUCTION_PREDICTION_QUARTERLY), SUM_PRODUCTION); - var predictor1 = new DummyPredictor("predictor0", componentManager, - Prediction.from(sum, SUM_CONSUMPTION, midnight, CONSUMPTION_PREDICTION_QUARTERLY), SUM_CONSUMPTION); - var timeOfUseTariff = DummyTimeOfUseTariffProvider.fromHourlyPrices(clock, HOURLY_PRICES_SUMMER); - var ctrl = new TimeOfUseTariffControllerImpl(); // this is not fully activated; config is null + final var predictor1 = new DummyPredictor("predictor1", componentManager, + Prediction.from(sum, SUM_UNMANAGED_CONSUMPTION, midnight, CONSUMPTION_PREDICTION_QUARTERLY), + SUM_UNMANAGED_CONSUMPTION); + final var timeOfUseTariff = DummyTimeOfUseTariffProvider.fromHourlyPrices(clock, HOURLY_PRICES_SUMMER); - var sut = new EnergySchedulerImpl(); + final var sut = new EnergySchedulerImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", componentManager) // .addReference("predictorManager", new DummyPredictorManager(predictor0, predictor1)) // .addReference("timedata", new DummyTimedata("timedata0")) // .addReference("timeOfUseTariff", timeOfUseTariff) // - .addReference("schedulables", List.of(ctrl)) // + .addReference("scheduler", new DummyScheduler("scheduler0")) // + .addReference("addSchedulable", + new DummyEnergySchedulable("ctrlEmergencyCapacityReserve0", + ControllerEssEmergencyCapacityReserveImpl.buildEnergyScheduleHandler(// + () -> /* reserveSoc */ 10))) // + .addReference("addSchedulable", + new DummyEnergySchedulable("ctrlLimitTotalDischarge0", + ControllerEssLimitTotalDischargeImpl.buildEnergyScheduleHandler(// + () -> /* minSoc */ 12))) // + .addReference("addSchedulable", + new DummyEnergySchedulable("ctrlFixActivePower0", + ControllerEssFixActivePowerImpl.buildEnergyScheduleHandler(// + () -> new ControllerEssFixActivePowerImpl.EshContext( + io.openems.edge.controller.ess.fixactivepower.Mode.MANUAL_ON, // + toEnergy(-1000), GREATER_OR_EQUALS)))) // + .addReference("addSchedulable", + new DummyEnergySchedulable("ctrlGridOptimizedCharge0", + ControllerEssGridOptimizedChargeImpl.buildEnergyScheduleHandler(// + () -> io.openems.edge.controller.ess.gridoptimizedcharge.Mode.MANUAL, // + () -> LocalTime.of(10, 00)))) // + .addReference("addSchedulable", + new DummyEnergySchedulable("ctrlEssTimeOfUseTariff0", + TimeOfUseTariffControllerImpl.buildEnergyScheduleHandler(// + () -> ess, // + () -> ControlMode.CHARGE_CONSUMPTION, // + () -> /* maxChargePowerFromGrid */ 20_000, // + () -> /* limitChargePowerFor14aEnWG */ false))) .addReference("sum", sum) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("_energy") // .setEnabled(false) // - .setEssId("ess0") // - .setEssMaxChargePower(5000) // - .setMaxChargePowerFromGrid(10000) // - .setLimitChargePowerFor14aEnWG(false) // + .setLogVerbosity(TRACE) // + .setVersion(V2_ENERGY_SCHEDULABLE) // .build()) // .next(new TestCase()); return sut; @@ -91,36 +127,7 @@ public static EnergySchedulerImpl create(Clock clock) throws Exception { * @throws Exception on error */ public static Optimizer getOptimizer(EnergySchedulerImpl energyScheduler) throws Exception { - var field = EnergySchedulerImpl.class.getDeclaredField("optimizer"); - field.setAccessible(true); - return (Optimizer) field.get(energyScheduler); - } - - /** - * Calls the 'createParams()' method in the {@link Optimizer} via Java - * Reflection. - * - * @param optimizer the {@link Optimizer} - * @throws Exception on error - */ - public static void callCreateParams(Optimizer optimizer) throws Exception { - var method = Optimizer.class.getDeclaredMethod("createParams"); - method.setAccessible(true); - method.invoke(optimizer); + return getValueViaReflection(energyScheduler, "optimizer"); } - /** - * Gets the {@link GlobalContext} via Java Reflection. - * - * @param energyScheduler the {@link EnergySchedulerImpl} - * @return the object - * @throws Exception on error - */ - @SuppressWarnings("unchecked") - public static GlobalContext getGlobalContext(EnergySchedulerImpl energyScheduler) throws Exception { - var optimizer = getOptimizer(energyScheduler); - var field = Optimizer.class.getDeclaredField("globalContext"); - field.setAccessible(true); - return ((Supplier) field.get(optimizer)).get(); - } } diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/MyConfig.java b/io.openems.edge.energy/test/io/openems/edge/energy/MyConfig.java index 2aeb9dc7bd3..e85ab323e37 100644 --- a/io.openems.edge.energy/test/io/openems/edge/energy/MyConfig.java +++ b/io.openems.edge.energy/test/io/openems/edge/energy/MyConfig.java @@ -1,17 +1,16 @@ package io.openems.edge.energy; import io.openems.common.test.AbstractComponentConfig; +import io.openems.edge.energy.api.Version; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { - protected static class Builder { + public static class Builder { private String id; private boolean enabled; - private String essId; - private int essMaxChargePower; - private int maxChargePowerFromGrid; - private boolean limitChargePowerFor14aEnWG; + private LogVerbosity logVerbosity; + private Version version; private Builder() { } @@ -26,23 +25,13 @@ public Builder setEnabled(boolean enabled) { return this; } - public Builder setEssId(String essId) { - this.essId = essId; + public Builder setLogVerbosity(LogVerbosity logVerbosity) { + this.logVerbosity = logVerbosity; return this; } - public Builder setEssMaxChargePower(int essMaxChargePower) { - this.essMaxChargePower = essMaxChargePower; - return this; - } - - public Builder setMaxChargePowerFromGrid(int maxChargePowerFromGrid) { - this.maxChargePowerFromGrid = maxChargePowerFromGrid; - return this; - } - - public Builder setLimitChargePowerFor14aEnWG(boolean limitChargePowerFor14aEnWG) { - this.limitChargePowerFor14aEnWG = limitChargePowerFor14aEnWG; + public Builder setVersion(Version version) { + this.version = version; return this; } @@ -71,4 +60,14 @@ private MyConfig(Builder builder) { public boolean enabled() { return this.builder.enabled; } + + @Override + public LogVerbosity logVerbosity() { + return this.builder.logVerbosity; + } + + @Override + public Version version() { + return this.builder.version; + } } \ No newline at end of file diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/api/simulation/EnergyFlowTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/api/simulation/EnergyFlowTest.java new file mode 100644 index 00000000000..b5eb2f38034 --- /dev/null +++ b/io.openems.edge.energy/test/io/openems/edge/energy/api/simulation/EnergyFlowTest.java @@ -0,0 +1,468 @@ +package io.openems.edge.energy.api.simulation; + +import static io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.applyBalancing; +import static io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.applyChargeGrid; +import static io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.applyDelayDischarge; +import static io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl.applyDischargeGrid; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class EnergyFlowTest { + + /* + * BALANCING + */ + + @Test + public void testBalancingAndCharge() { + var m = new EnergyFlow.Model(// + /* production */ 2500, // + /* consumption */ 500, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 0, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyBalancing(m); + var ef = m.solve(); + + assertEquals(500, ef.getCons()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(2500, ef.getProd()); + assertEquals(500, ef.getProdToCons()); + assertEquals(2000, ef.getProdToEss()); + + assertEquals(-2000, ef.getEss()); + assertEquals(2000, ef.getProdToEss()); + + assertEquals(0, ef.getGrid()); + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getGridToCons()); + assertEquals(0, ef.getEssToCons()); + assertEquals(0, ef.getGridToEss()); + } + + @Test + public void testBalancingAndChargeFull() { + var m = new EnergyFlow.Model(// + /* production */ 3000, // + /* consumption */ 100, // + /* essMaxCharge */ 2400, // + /* essMaxDischarge */ 0, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyBalancing(m); + var ef = m.solve(); + + assertEquals(100, ef.getCons()); + assertEquals(100, ef.getProdToCons()); + + assertEquals(3000, ef.getProd()); + assertEquals(100, ef.getProdToCons()); + assertEquals(2400, ef.getProdToEss()); + assertEquals(500, ef.getProdToGrid()); + + assertEquals(-2400, ef.getEss()); + assertEquals(2400, ef.getProdToEss()); + + assertEquals(-500, ef.getGrid()); + assertEquals(500, ef.getProdToGrid()); + + assertEquals(0, ef.getGridToCons()); + assertEquals(0, ef.getEssToCons()); + assertEquals(0, ef.getGridToEss()); + } + + @Test + public void testBalancingAndDischarge() { + var m = new EnergyFlow.Model(// + /* production */ 500, // + /* consumption */ 2500, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 5000, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyBalancing(m); + var ef = m.solve(); + + assertEquals(2500, ef.getCons()); + assertEquals(2000, ef.getEssToCons()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(500, ef.getProd()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(2000, ef.getEss()); + assertEquals(2000, ef.getEssToCons()); + + assertEquals(0, ef.getGrid()); + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getProdToEss()); + assertEquals(0, ef.getGridToCons()); + assertEquals(0, ef.getGridToEss()); + } + + @Test + public void testBalancingAndDischargeEmpty() { + var m = new EnergyFlow.Model(// + /* production */ 500, // + /* consumption */ 4500, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 1800, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyBalancing(m); + var ef = m.solve(); + + assertEquals(4500, ef.getCons()); + assertEquals(2200, ef.getGridToCons()); + assertEquals(1800, ef.getEssToCons()); + + assertEquals(500, ef.getProd()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(1800, ef.getEss()); + assertEquals(1800, ef.getEssToCons()); + + assertEquals(2200, ef.getGrid()); + assertEquals(2200, ef.getGridToCons()); + + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getProdToEss()); + assertEquals(0, ef.getGridToEss()); + } + + @Test + public void testBalancingAndChargeMoreThanEssMaxEnergy() { + var m = new EnergyFlow.Model(// + /* production */ 2500, // + /* consumption */ 500, // + /* essMaxCharge */ 900, // + /* essMaxDischarge */ 900, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyBalancing(m); + var ef = m.solve(); + + assertEquals(500, ef.getCons()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(2500, ef.getProd()); + assertEquals(500, ef.getProdToCons()); + assertEquals(900, ef.getProdToEss()); + assertEquals(1100, ef.getProdToGrid()); + + assertEquals(-900, ef.getEss()); + assertEquals(900, ef.getProdToEss()); + + assertEquals(-1100, ef.getGrid()); + assertEquals(1100, ef.getProdToGrid()); + + assertEquals(0, ef.getGridToCons()); + assertEquals(0, ef.getEssToCons()); + assertEquals(0, ef.getGridToEss()); + } + + @Test + public void testBalancingAndDischargeAboveEssMaxEnergy() { + var m = new EnergyFlow.Model(// + /* production */ 500, // + /* consumption */ 2500, // + /* essMaxCharge */ 900, // + /* essMaxDischarge */ 900, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyBalancing(m); + var ef = m.solve(); + + assertEquals(2500, ef.getCons()); + assertEquals(900, ef.getEssToCons()); + assertEquals(1100, ef.getGridToCons()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(500, ef.getProd()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(900, ef.getEss()); + assertEquals(900, ef.getEssToCons()); + + assertEquals(1100, ef.getGrid()); + assertEquals(1100, ef.getGridToCons()); + + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getProdToEss()); + assertEquals(0, ef.getGridToEss()); + } + + @Test + public void testBalancingAndAboveGridMaxEnergy() { + var m = new EnergyFlow.Model(// + /* production */ 1000, // + /* consumption */ 4900, // + /* essMaxCharge */ 1600, // + /* essMaxDischarge */ 2000, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyBalancing(m); + var ef = m.solve(); + + assertEquals(4900, ef.getCons()); + assertEquals(2000, ef.getEssToCons()); + assertEquals(1900, ef.getGridToCons()); + assertEquals(1000, ef.getProdToCons()); + + assertEquals(1000, ef.getProd()); + assertEquals(1000, ef.getProdToCons()); + + assertEquals(2000, ef.getEss()); + assertEquals(2000, ef.getEssToCons()); + + assertEquals(1900, ef.getGrid()); + assertEquals(1900, ef.getGridToCons()); + + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getProdToEss()); + assertEquals(0, ef.getGridToEss()); + } + + /* + * DELAY DISCHARGE + */ + + @Test + public void testDelayDischargeAndCharge() { + var m = new EnergyFlow.Model(// + /* production */ 2500, // + /* consumption */ 500, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 0, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyDelayDischarge(m); + var ef = m.solve(); + + assertEquals(500, ef.getCons()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(2500, ef.getProd()); + assertEquals(2000, ef.getProdToEss()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(-2000, ef.getEss()); + assertEquals(2000, ef.getProdToEss()); + + assertEquals(0, ef.getGrid()); + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getGridToCons()); + assertEquals(0, ef.getEssToCons()); + assertEquals(0, ef.getGridToEss()); + } + + @Test + public void testDelayDischargeAndChargeFull() { + var m = new EnergyFlow.Model(// + /* production */ 3000, // + /* consumption */ 100, // + /* essMaxCharge */ 2400, // + /* essMaxDischarge */ 5000, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyDelayDischarge(m); + var ef = m.solve(); + + assertEquals(100, ef.getCons()); + assertEquals(100, ef.getProdToCons()); + + assertEquals(3000, ef.getProd()); + assertEquals(100, ef.getProdToCons()); + assertEquals(500, ef.getProdToGrid()); + assertEquals(2400, ef.getProdToEss()); + + assertEquals(-2400, ef.getEss()); + assertEquals(2400, ef.getProdToEss()); + + assertEquals(-500, ef.getGrid()); + assertEquals(500, ef.getProdToGrid()); + + assertEquals(0, ef.getGridToCons()); + assertEquals(0, ef.getEssToCons()); + assertEquals(0, ef.getGridToEss()); + } + + @Test + public void testDelayDischargeAndWouldDischarge() { + var m = new EnergyFlow.Model(// + /* production */ 500, // + /* consumption */ 2500, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 5000, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyDelayDischarge(m); + var ef = m.solve(); + + assertEquals(2500, ef.getCons()); + assertEquals(500, ef.getProdToCons()); + assertEquals(2000, ef.getGridToCons()); + + assertEquals(500, ef.getProd()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(2000, ef.getGrid()); + assertEquals(2000, ef.getGridToCons()); + + assertEquals(0, ef.getEss()); + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getProdToEss()); + assertEquals(0, ef.getEssToCons()); + assertEquals(0, ef.getGridToEss()); + } + + /* + * CHARGE GRID + */ + + @Test + public void testChargeGridAndCharge() { + var m = new EnergyFlow.Model(// + /* production */ 2500, // + /* consumption */ 500, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 0, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyChargeGrid(m, 2500); + var ef = m.solve(); + + assertEquals(500, ef.getCons()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(2500, ef.getProd()); + assertEquals(2000, ef.getProdToEss()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(-4500, ef.getEss()); + assertEquals(2500, ef.getGridToEss()); + assertEquals(2000, ef.getProdToEss()); + + assertEquals(2500, ef.getGrid()); + assertEquals(2500, ef.getGridToEss()); + + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getGridToCons()); + assertEquals(0, ef.getEssToCons()); + } + + @Test + public void testChargeGridAndChargeFull() { + var m = new EnergyFlow.Model(// + /* production */ 3000, // + /* consumption */ 100, // + /* essMaxCharge */ 3400, // + /* essMaxDischarge */ 5000, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyChargeGrid(m, 2500); + var ef = m.solve(); + + assertEquals(100, ef.getCons()); + assertEquals(100, ef.getProdToCons()); + + assertEquals(3000, ef.getProd()); + assertEquals(100, ef.getProdToCons()); + assertEquals(2900, ef.getProdToEss()); + + assertEquals(-3400, ef.getEss()); + assertEquals(500, ef.getGridToEss()); + assertEquals(2900, ef.getProdToEss()); + + assertEquals(500, ef.getGrid()); + assertEquals(500, ef.getGridToEss()); + + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getGridToCons()); + assertEquals(0, ef.getEssToCons()); + } + + @Test + public void testChargeGridAndAboveGridMaxEnergy() { + var m = new EnergyFlow.Model(// + /* production */ 1000, // + /* consumption */ 2000, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 5000, // + /* gridMaxBuy */ 1600, // + /* gridMaxSell */ 10000); + applyChargeGrid(m, 2500); + var ef = m.solve(); + + assertEquals(2000, ef.getCons()); + assertEquals(1000, ef.getProdToCons()); + assertEquals(1000, ef.getGridToCons()); + + assertEquals(1000, ef.getProd()); + assertEquals(1000, ef.getGridToCons()); + + assertEquals(-600, ef.getEss()); + assertEquals(600, ef.getGridToEss()); + + assertEquals(1600, ef.getGrid()); + assertEquals(1000, ef.getGridToCons()); + assertEquals(600, ef.getGridToEss()); + + assertEquals(0, ef.getProdToGrid()); + assertEquals(0, ef.getProdToEss()); + assertEquals(0, ef.getEssToCons()); + } + + /* + * DISCHARGE GRID - just for completeness + */ + + @Test + public void testDischargeGridAndCharge() { + var m = new EnergyFlow.Model(// + /* production */ 2500, // + /* consumption */ 500, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 5000, // + /* gridMaxBuy */ 1600, // + /* gridMaxSell */ 10000); + applyDischargeGrid(m, 2500); + var ef = m.solve(); + + assertEquals(500, ef.getCons()); + assertEquals(500, ef.getProdToCons()); + + assertEquals(2500, ef.getProd()); + assertEquals(500, ef.getProdToCons()); + assertEquals(2000, ef.getProdToGrid()); + + assertEquals(2500, ef.getEss()); + assertEquals(-2500, ef.getGridToEss()); + + assertEquals(-4500, ef.getGrid()); + assertEquals(2000, ef.getProdToGrid()); + assertEquals(-2500, ef.getGridToEss()); + + assertEquals(0, ef.getProdToEss()); + assertEquals(0, ef.getEssToCons()); + assertEquals(0, ef.getGridToCons()); + } + + @Test + public void testLog() { + // No actual test. Would have to mock Logger + var m = new EnergyFlow.Model(// + /* production */ 2500, // + /* consumption */ 500, // + /* essMaxCharge */ 5000, // + /* essMaxDischarge */ 0, // + /* gridMaxBuy */ 4000, // + /* gridMaxSell */ 10000); + applyBalancing(m); + m.logConstraints(); + m.logMinMaxValues(); + } +} diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/OptimizerTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/OptimizerTest.java index 1ab693e4e03..e2c2dd7d3c1 100644 --- a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/OptimizerTest.java +++ b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/OptimizerTest.java @@ -1,21 +1,74 @@ package io.openems.edge.energy.optimizer; +import static io.openems.common.utils.ReflectionUtils.getValueViaReflection; import static io.openems.edge.energy.EnergySchedulerImplTest.CLOCK; import static io.openems.edge.energy.EnergySchedulerImplTest.getOptimizer; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import org.junit.Test; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.function.ThrowingSupplier; +import io.openems.common.utils.ReflectionUtils.ReflectionException; +import io.openems.edge.controller.ess.timeofusetariff.StateMachine; import io.openems.edge.energy.EnergySchedulerImplTest; +import io.openems.edge.energy.LogVerbosity; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; public class OptimizerTest { @Test - public void testEmpty() throws Exception { - var sut = getOptimizer(EnergySchedulerImplTest.create(CLOCK)); - assertNull(sut.getParams()); - assertTrue(sut.getSchedule().isEmpty()); + public void test() throws Exception { + var sut = EnergySchedulerImplTest.create(CLOCK); + var optimizer = getOptimizer(sut); + assertEquals("No Schedule available|PerQuarter:UNDEFINED", optimizer.debugLog()); + + var gscSupplier = getGlobalSimulationContextSupplier(optimizer); + var simulator = new Simulator(gscSupplier.get()); + optimizer.runOnce(simulator); + + assertEquals("ScheduledPeriods:96|PerQuarter:UNDEFINED", optimizer.debugLog()); + + var sr = optimizer.getSimulationResult(); + assertEquals(1375977.5150000001, sr.cost(), 0.001); + assertEquals(96, sr.periods().size()); + + var ctrlEssTimeOfUseTariff0 = sr.schedules().entrySet().asList().get(0); + var p = ctrlEssTimeOfUseTariff0.getKey().getCurrentPeriod(); + assertEquals(StateMachine.CHARGE_GRID, p.state()); + } + + @Test + public void test2() { + var simulator = SimulatorTest.DUMMY_SIMULATOR; + var o = new Optimizer(// + () -> LogVerbosity.NONE, // + () -> simulator.gsc, // + null); + o.applyBestQuickSchedule(simulator); + + var schedule = ((EnergyScheduleHandler.WithDifferentStates) simulator.gsc.handlers().get(1)) + .getSchedule(); + + assertEquals(52, schedule.size()); + + assertTrue(schedule.values().stream() // + .allMatch(p -> p.state() == StateMachine.BALANCING)); + } + + /** + * Gets the {@link GlobalSimulationsContext} {@link ThrowingSupplier} via Java + * Reflection. + * + * @param optimizer the {@link Optimizer} + * @return the object + * @throws ReflectionException on error + */ + public static ThrowingSupplier getGlobalSimulationContextSupplier( + Optimizer optimizer) throws ReflectionException { + return getValueViaReflection(optimizer, "gscSupplier"); } } diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/QuickSchedulesTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/QuickSchedulesTest.java new file mode 100644 index 00000000000..d9ad2be088d --- /dev/null +++ b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/QuickSchedulesTest.java @@ -0,0 +1,65 @@ +package io.openems.edge.energy.optimizer; + +import static io.openems.edge.energy.optimizer.QuickSchedules.fromExistingSimulationResult; +import static io.openems.edge.energy.optimizer.QuickSchedules.variationsFromExistingSimulationResult; +import static io.openems.edge.energy.optimizer.QuickSchedules.variationsOfAllStatesDefault; +import static io.openems.edge.energy.optimizer.SimulatorTest.ESH_TIME_OF_USE_TARIFF_CTRL; +import static org.junit.Assert.assertEquals; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import org.junit.Test; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; + +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.EnergyScheduleHandler.WithDifferentStates.Period.Transition; + +public class QuickSchedulesTest { + + private static final ZonedDateTime TIME = ZonedDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")); + + @Test + public void testAllStatesDefault() { + final var simulator = SimulatorTest.DUMMY_SIMULATOR; + var gts = variationsOfAllStatesDefault(simulator.gsc).toList(); + assertEquals(6, gts.size()); + var gt = gts.get(0); + assertEquals(0, gt.get(0).get(1).allele().intValue()); + } + + @Test + public void testFromExistingSimulationResult() { + final var simulator = SimulatorTest.DUMMY_SIMULATOR; + final var previousResult = new SimulationResult(0., ImmutableMap.of(), // + ImmutableMap., ImmutableSortedMap>builder() // + .put(ESH_TIME_OF_USE_TARIFF_CTRL, ImmutableSortedMap.naturalOrder() // + .put(TIME.plusHours(0).plusMinutes(00), state(2)) // + .build()) // + .build()); + var gt = fromExistingSimulationResult(simulator.gsc, previousResult); + assertEquals(2, gt.get(0).get(0).allele().intValue()); + } + + @Test + public void testVariationsFromExistingSimulationResult() { + final var simulator = SimulatorTest.DUMMY_SIMULATOR; + final var previousResult = new SimulationResult(0., ImmutableMap.of(), // + ImmutableMap., ImmutableSortedMap>builder() // + .put(ESH_TIME_OF_USE_TARIFF_CTRL, ImmutableSortedMap.naturalOrder() // + .put(TIME.plusHours(0).plusMinutes(00), state(2)) // + .put(TIME.plusHours(0).plusMinutes(15), state(2)) // + .build()) // + .build()); + var gts = variationsFromExistingSimulationResult(simulator.gsc, previousResult).toList(); + assertEquals(6, gts.size()); + var gt = gts.get(0); + assertEquals(2, gt.get(0).get(1).allele().intValue()); + } + + protected static Transition state(int state) { + return new Transition(state, 0., null, 0); + } +} diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/SimulationResultTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/SimulationResultTest.java new file mode 100644 index 00000000000..bb4f49f9cd3 --- /dev/null +++ b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/SimulationResultTest.java @@ -0,0 +1,56 @@ +package io.openems.edge.energy.optimizer; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.stream.IntStream; + +import org.junit.Test; + +import io.jenetics.Genotype; +import io.jenetics.IntegerChromosome; +import io.jenetics.IntegerGene; + +public class SimulationResultTest { + + @Test + public void test() { + final var simulator = SimulatorTest.DUMMY_SIMULATOR; + + var result = SimulationResult.fromQuarters(simulator.gsc, Genotype.of(// + // ESH1 (BALANCING, DELAY_DISCHARGE, CHARGE_GRID) + integerChromosomeOf(// + 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 1, 2), // + // ESH2 (FOO, BAR) + integerChromosomeOf(// + 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 1, 0))); + + assertEquals(1_126_154.844, result.cost(), 0.001); + } + + /** + * Creates a {@link IntegerChromosome} from the given values. + * + * @param values the int values + * @return the {@link IntegerChromosome} + */ + public static IntegerChromosome integerChromosomeOf(int... values) { + if (values.length == 0) { + return IntegerChromosome.of(); + } + var max = IntStream.of(values).max().getAsInt(); + return IntegerChromosome.of(Arrays.stream(values) // + .mapToObj(value -> IntegerGene.of(value, 0, max)) // + .toList()); + } +} diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/SimulatorTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/SimulatorTest.java index 73cae4f79cb..06e1e98d859 100644 --- a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/SimulatorTest.java +++ b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/SimulatorTest.java @@ -1,40 +1,56 @@ package io.openems.edge.energy.optimizer; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.CHARGE_GRID; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; -import static io.openems.edge.energy.TestData.CONSUMPTION_888_20231106; -import static io.openems.edge.energy.TestData.PRICES_888_20231106; -import static io.openems.edge.energy.TestData.PRODUCTION_888_20231106; -import static io.openems.edge.energy.optimizer.Simulator.getBestSchedule; -import static io.openems.edge.energy.optimizer.Simulator.simulate; -import static io.openems.edge.energy.optimizer.Utils.interpolateArray; -import static io.openems.edge.energy.optimizer.Utils.toEnergy; -import static java.util.Arrays.stream; -import static org.junit.Assert.assertArrayEquals; +import static io.openems.edge.energy.api.EnergyUtils.socToEnergy; import static org.junit.Assert.assertEquals; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Arrays; import java.util.Random; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.DoubleStream; import org.junit.Before; import org.junit.Test; -import com.google.common.collect.ImmutableSortedMap; - +import io.jenetics.engine.Limits; import io.jenetics.util.RandomRegistry; import io.openems.edge.controller.ess.timeofusetariff.ControlMode; -import io.openems.edge.controller.ess.timeofusetariff.StateMachine; -import io.openems.edge.energy.optimizer.Simulator.Period; +import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.test.DummyGlobalSimulationsContext; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; public class SimulatorTest { - public static final ZonedDateTime TIME = ZonedDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")); + public static final EnergyScheduleHandler.WithOnlyOneState ESH0 = EnergyScheduleHandler.of(// + simContext -> simContext.ess().totalEnergy(), // + (simContext, period, energyFlow, ctrlContext) -> { + var minEnergy = socToEnergy(simContext.global.ess().totalEnergy(), 10 /* [%] */); + energyFlow.setEssMaxDischarge(Math.max(0, simContext.getEssInitial() - minEnergy)); + }); + + public static final ManagedSymmetricEss ESS = new DummyManagedSymmetricEss("ess0") // + .withMaxApparentPower(10_000) // + .withAllowedChargePower(8_000) // + .withAllowedDischargePower(8_000) // + .withCapacity(22_000); + public static final EnergyScheduleHandler.WithDifferentStates ESH_TIME_OF_USE_TARIFF_CTRL = TimeOfUseTariffControllerImpl + .buildEnergyScheduleHandler(// + () -> ESS, // + () -> ControlMode.CHARGE_CONSUMPTION, // + () -> 20_000 /* maxChargePowerFromGrid */, // + () -> false /* limitChargePowerFor14aEnWG */); + + private static enum Esh2State { + FOO, BAR; + } + + public static final EnergyScheduleHandler.WithDifferentStates ESH2 = EnergyScheduleHandler.of(// + Esh2State.BAR, // + () -> Esh2State.values(), // + simContext -> null, // + (simContext, period, energyFlow, ctrlContext, state) -> { + }); + + public static final Simulator DUMMY_SIMULATOR = new Simulator(// + DummyGlobalSimulationsContext.fromHandlers(ESH0, ESH_TIME_OF_USE_TARIFF_CTRL, ESH2)); @Before public void before() { @@ -43,164 +59,32 @@ public void before() { RandomRegistry.random(new Random(123)); } - private static Period simulatePeriod(StateMachine state, int production, int consumption, double price, - int essInitial) { - var result = new AtomicReference(); - var params = Params.create() // - .setTime(TIME) // - .setEssTotalEnergy(22000) // - .setEssMinSocEnergy(0) // - .setEssMaxSocEnergy(20000) // - .setEssInitialEnergy(essInitial) // - .setEssMaxChargeEnergy(3000 /* [Wh/15 Minutes] */) // - .setEssMaxDischargeEnergy(3000 /* [Wh/15 Minutes] */) // - .seMaxBuyFromGrid(4000 /* [Wh/15 Minutes] */) // - .setProductions(new int[] { production }) // - .setConsumptions(new int[] { consumption }) // - .setPrices(new double[] { price }) // - .setStates(new StateMachine[] { state }) // - .setExistingSchedule(ImmutableSortedMap.of()) // - .build(); - Simulator.simulatePeriod(params, params.optimizePeriods().get(0), state, new AtomicInteger(essInitial), - result::set); - - return result.get(); - } - - private static void assertPeriod(String message, Period period, int essChargeDischarge, int grid, double cost) { - assertEquals(period.state() + "-essChargeDischarge: " + message, essChargeDischarge, period.ef().ess()); - assertEquals(period.state() + "-grid: " + message, grid, period.ef().grid()); - } - - @Test - public void testCalculatePeriodCostBalancing() { - assertPeriod("Consumption > Production; SoC ok", // - simulatePeriod(BALANCING, 200, 300, 0.1, 10000), // - 100, 0, 0); - assertPeriod("Consumption > Production; discharge limited by essMaxEnergyPerPeriod", // - simulatePeriod(BALANCING, 1000, 5000, 0.1, 10000), // - 3000, 1000, 100); - assertPeriod("Consumption > Production; discharge limited by essMinSocEnergy", // - simulatePeriod(BALANCING, 1000, 5000, 0.1, 2500), // - 2500, 1500, 150); - - assertPeriod("Production > Consumption; SoC ok", // - simulatePeriod(BALANCING, 300, 200, 0.1, 10000), // - -100, 0, 0); - assertPeriod("Production > Consumption; charge limited by essMaxEnergyPerPeriod", // - simulatePeriod(BALANCING, 5000, 1000, 0.1, 10000), // - -3000, -1000, 0); - assertPeriod("Production > Consumption; charge limited by essTotalEnergy", // - simulatePeriod(BALANCING, 5000, 1000, 0.1, 19500), // - -2500, -1500, 0); - } - - @Test - public void testCalculatePeriodCostDelayDischarge() { - assertPeriod("Consumption > Production", // - simulatePeriod(DELAY_DISCHARGE, 200, 300, 0.1, 10000), // - 0, 100, 10); - - assertPeriod("Production > Consumption; SoC ok", // - simulatePeriod(DELAY_DISCHARGE, 300, 200, 0.1, 10000), // - -100, 0, 0); - assertPeriod("Production > Consumption; charge limited by essMaxEnergyPerPeriod", // - simulatePeriod(DELAY_DISCHARGE, 5000, 1000, 0.1, 10000), // - -3000, -1000, 0); - assertPeriod("Production > Consumption; charge limited by essTotalEnergy", // - simulatePeriod(DELAY_DISCHARGE, 5000, 1000, 0.1, 19500), // - -2500, -1500, 0); - } - - @Test - public void testCalculatePeriodCostChargeGrid() { - assertPeriod("Consumption > Production", // - simulatePeriod(CHARGE_GRID, 200, 300, 0.1, 10000), // - -842, 942 /* 842 + 100 */, 302.5); - - assertPeriod("Consumption > Production; charge limited by maxBuyFromGrid", // - simulatePeriod(CHARGE_GRID, 0, 4500, 0.1, 10000), // - 500, 4000, 450.12); - - assertPeriod("Production > Consumption", // - simulatePeriod(CHARGE_GRID, 300, 200, 0.1, 10000), // - -2600 /* 2500 + 100 */, 2500, 292.5); - - assertPeriod("Production > Consumption; charge limited by essMaxEnergyPerPeriod", // - simulatePeriod(CHARGE_GRID, 3000, 900, 0.1, 10000), // - -3000, 900, 105.3); - - assertPeriod("Production > Consumption", // - simulatePeriod(CHARGE_GRID, 2000, 1700, 0.1, 10000), // - -2800, 2500, 292.5); - - assertPeriod("Production > Consumption; battery nearly full", // - simulatePeriod(CHARGE_GRID, 3000, 100, 0.1, 19600), // - -400 /* 400 from PV; then full */, -2500 /* sell-to-grid */, 292.5); - } - - @Test - public void testGetFirstSchedule0() { - var existingSchedule = new StateMachine[] { CHARGE_GRID, DELAY_DISCHARGE, CHARGE_GRID, BALANCING }; - - var p = Params.create() // - .setTime(TIME) // - .setEssTotalEnergy(22000) // - .setEssMinSocEnergy(0) // - .setEssMaxSocEnergy(22000) // - .setEssInitialEnergy((int) (22000 * 0.1)) // - .setEssMaxChargeEnergy(toEnergy(10000)) // - .setEssMaxDischargeEnergy(toEnergy(10000)) // - .seMaxBuyFromGrid(toEnergy(24_000)) // - .setProductions(stream(interpolateArray(PRODUCTION_888_20231106)).map(v -> toEnergy(v)).toArray()) // - .setConsumptions(stream(interpolateArray(CONSUMPTION_888_20231106)).map(v -> toEnergy(v)).toArray()) // - .setPrices(hourlyToQuarterly(interpolateArray(PRICES_888_20231106))) // - .setStates(ControlMode.CHARGE_CONSUMPTION.states) // - .setExistingSchedule(UtilsTest.prepareExistingSchedule(TIME, existingSchedule)) // - .build(); - var s = getBestSchedule(p, // - /* executionLimitSeconds */ 30, // - /* populationSize */ 2, // - /* limit */ 1); - - assertArrayEquals(existingSchedule, Arrays.copyOfRange(s, 0, existingSchedule.length)); - } - /** - * Creates dummy {@link Params}. + * Generates a dummy {@link SimulationResult}. * - * @param states the allowed states - * @return {@link Params} + * @return the {@link SimulationResult} */ - public static Params createParams888d20231106(StateMachine... states) { - return Params.create() // - .setTime(TIME) // - .setEssTotalEnergy(22000) // - .setEssMinSocEnergy(0) // - .setEssMaxSocEnergy(22000) // - .setEssMaxChargeEnergy(toEnergy(10000)) // - .setEssMaxDischargeEnergy(toEnergy(10000)) // - .seMaxBuyFromGrid(toEnergy(24_000)) // - .setProductions(stream(interpolateArray(PRODUCTION_888_20231106)).map(v -> toEnergy(v)).toArray()) // - .setConsumptions(stream(interpolateArray(CONSUMPTION_888_20231106)).map(v -> toEnergy(v)).toArray()) // - .setPrices(hourlyToQuarterly(interpolateArray(PRICES_888_20231106))) // - .setStates(states) // - .build(); + public static SimulationResult generateDummySimulationResult() { + final var simulator = DUMMY_SIMULATOR; + + return simulator.getBestSchedule(SimulationResult.EMPTY, // + engine -> engine // + .populationSize(1), // + stream -> stream // + .limit(Limits.byFixedGeneration(1))); } - protected static void logSchedule(Params p, StateMachine[] schedule) { - Utils.logSchedule(p, simulate(p, schedule)); - } + @Test + public void testGetBestSchedule() { + var simulationResult = generateDummySimulationResult(); - /** - * Convert hourly values to quarterly. - * - * @param values hourly values - * @return quarterly values - */ - protected static double[] hourlyToQuarterly(double[] values) { - return DoubleStream.of(values) // - .flatMap(v -> DoubleStream.of(v, v, v, v)) // - .toArray(); + assertEquals(2, simulationResult.schedules().size()); + + simulationResult.schedules().forEach((esh, schedule) -> { + esh.applySchedule(schedule); + }); + + assertEquals("BALANCING", ESH_TIME_OF_USE_TARIFF_CTRL.getCurrentPeriod().state().toString()); + assertEquals("BAR", ESH2.getCurrentPeriod().state().toString()); } } \ No newline at end of file diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/TestData.java b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/TestData.java new file mode 100644 index 00000000000..920ff079c13 --- /dev/null +++ b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/TestData.java @@ -0,0 +1,67 @@ +package io.openems.edge.energy.optimizer; + +public class TestData { + + public static final Integer[] PRODUCTION_PREDICTION_QUARTERLY = { + /* 00:00-03:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 04:00-07:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 297, 610, // + /* 08:00-11:45 */ + 913, 1399, 1838, 2261, 2662, 3052, 3405, 3708, 4011, 4270, 4458, 4630, 4794, 4908, 4963, 4960, // + /* 12:00-15:45 */ + 4973, 4940, 4859, 4807, 4698, 4530, 4348, 4147, 1296, 1399, 1838, 1261, 1662, 1052, 1405, 1402, + /* 16:00-19:45 */ + 1662, 1052, 1405, 1630, 1285, 1520, 1250, 910, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 20:00-23:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 00:00-03:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 04:00-07:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 130, 402, 667, // + /* 08:00-11:45 */ + 1023, 1631, 2020, 2420, 2834, 3237, 3638, 4006, 4338, 4597, 4825, 4965, 5111, 5213, 5268, 5317, // + /* 12:00-15:45 */ + 5321, 5271, 5232, 5193, 5044, 4915, 4738, 4499, 3702, 3226, 3046, 2857, 2649, 2421, 2184, 1933, // + /* 16:00-19:45 */ + 1674, 1364, 1070, 754, 447, 193, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 20:00-23:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // + }; + + public static final Integer[] CONSUMPTION_PREDICTION_QUARTERLY = { + /* 00:00-03:450 */ + 1021, 1208, 713, 931, 2847, 2551, 1558, 1234, 433, 633, 1355, 606, 430, 1432, 1121, 502, // + /* 04:00-07:45 */ + 294, 1048, 1194, 914, 1534, 1226, 1235, 977, 578, 1253, 1983, 1417, 513, 929, 1102, 445, // + /* 08:00-11:45 */ + 1208, 2791, 2729, 2609, 2086, 1454, 848, 816, 2610, 3150, 2036, 1180, 359, 1316, 3447, 2104, // + /* 12:00-15:45 */ + 905, 802, 828, 812, 863, 633, 293, 379, 1250, 2296, 2436, 2140, 2135, 1196, 2230, 1725, + /* 16:00-19:45 */ + 2365, 1758, 2325, 2264, 2181, 2167, 2228, 1082, 777, 417, 798, 1268, 409, 830, 1191, 417, // + /* 20:00-23:45 */ + 1087, 2958, 2946, 2235, 1343, 483, 796, 1201, 567, 395, 989, 1066, 370, 989, 1255, 660, // + /* 00:00-03:45 */ + 349, 880, 1186, 580, 327, 911, 1135, 553, 265, 938, 1165, 567, 278, 863, 1239, 658, // + /* 04:00-07:45 */ + 236, 816, 1173, 1131, 498, 550, 1344, 1226, 874, 504, 1733, 1809, 1576, 369, 771, 2583, // + /* 08:00-11:45 */ + 3202, 2174, 1878, 2132, 2109, 1895, 1565, 1477, 1613, 1716, 1867, 1726, 1700, 1787, 1755, 1734, // + /* 12:00-15:45 */ + 1380, 691, 338, 168, 199, 448, 662, 205, 183, 70, 169, 276, 149, 76, 195, 168, // + /* 16:00-19:45 */ + 159, 266, 135, 120, 224, 979, 2965, 1337, 1116, 795, 334, 390, 433, 369, 762, 2908, // + /* 20:00-23:45 */ + 3226, 2358, 1778, 1002, 455, 654, 534, 1587, 1638, 459, 330, 258, 368, 728, 1096, 878 // + }; + + public static final Double[] HOURLY_PRICES_SUMMER = { // + 70.95, 71.98, 71.95, 74.96, // + 78.93, 80., 84.01, 111.03, // + 105.04, 105., 74.23, 73.28, // + 67.97, 72.53, 89.66, 150.01, // + 173.54, 178.4, 158.91, 140.01, // + 149.99, 157.43, 130.9, 120.14 // + }; +} diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/UtilsTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/UtilsTest.java index a2b82ad90a9..58daf14c3ca 100644 --- a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/UtilsTest.java +++ b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/UtilsTest.java @@ -1,285 +1,25 @@ package io.openems.edge.energy.optimizer; -import static io.openems.common.utils.DateUtils.roundDownToQuarter; -import static io.openems.edge.common.test.TestUtils.withValue; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING; -import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE; -import static io.openems.edge.energy.EnergySchedulerImplTest.getOptimizer; -import static io.openems.edge.energy.TestData.CONSUMPTION_PREDICTION_QUARTERLY; -import static io.openems.edge.energy.TestData.PAST_HOURLY_PRICES; -import static io.openems.edge.energy.TestData.PAST_SOC; -import static io.openems.edge.energy.TestData.PAST_STATES; -import static io.openems.edge.energy.TestData.PRODUCTION_888_20231106; -import static io.openems.edge.energy.TestData.PRODUCTION_PREDICTION_QUARTERLY; -import static io.openems.edge.energy.optimizer.EnergyFlowTest.NO_FLOW; -import static io.openems.edge.energy.optimizer.SimulatorTest.TIME; -import static io.openems.edge.energy.optimizer.Utils.SUM_CONSUMPTION; -import static io.openems.edge.energy.optimizer.Utils.SUM_ESS_DISCHARGE_POWER; -import static io.openems.edge.energy.optimizer.Utils.SUM_ESS_SOC; -import static io.openems.edge.energy.optimizer.Utils.SUM_GRID; -import static io.openems.edge.energy.optimizer.Utils.SUM_PRODUCTION; import static io.openems.edge.energy.optimizer.Utils.calculateExecutionLimitSeconds; -import static io.openems.edge.energy.optimizer.Utils.findFirstPeakIndex; -import static io.openems.edge.energy.optimizer.Utils.findFirstValleyIndex; -import static io.openems.edge.energy.optimizer.Utils.generateProductionPrediction; -import static io.openems.edge.energy.optimizer.Utils.getEssMinSocEnergy; -import static io.openems.edge.energy.optimizer.Utils.interpolateArray; -import static io.openems.edge.energy.optimizer.Utils.joinConsumptionPredictions; -import static io.openems.edge.energy.optimizer.Utils.paramsAreValid; -import static io.openems.edge.energy.optimizer.Utils.toEnergy; -import static io.openems.edge.energy.optimizer.Utils.toPower; -import static io.openems.edge.energy.optimizer.Utils.updateSchedule; +import static io.openems.edge.energy.optimizer.Utils.sortByScheduler; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import java.time.Duration; import java.time.Instant; -import java.time.ZoneId; import java.time.ZoneOffset; -import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.TreeMap; -import java.util.stream.IntStream; +import java.util.stream.Stream; import org.junit.Test; -import com.google.common.collect.ImmutableSortedMap; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.test.TimeLeapClock; -import io.openems.common.types.ChannelAddress; -import io.openems.edge.common.component.OpenemsComponent; -import io.openems.edge.common.test.AbstractDummyOpenemsComponent; -import io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve; -import io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischarge; -import io.openems.edge.controller.ess.timeofusetariff.StateMachine; -import io.openems.edge.controller.ess.timeofusetariff.TimeOfUseTariffControllerImpl; -import io.openems.edge.energy.EnergySchedulerImplTest; -import io.openems.edge.energy.optimizer.Simulator.Period; -import io.openems.edge.timedata.test.DummyTimedata; +import io.openems.edge.energy.api.EnergySchedulable; +import io.openems.edge.energy.api.test.DummyEnergySchedulable; +import io.openems.edge.scheduler.api.test.DummyScheduler; public class UtilsTest { - protected static ImmutableSortedMap prepareExistingSchedule(ZonedDateTime fromDate, - StateMachine... existingSchedule) { - return IntStream.range(0, existingSchedule.length) // - .mapToObj(Integer::valueOf) // - .collect(ImmutableSortedMap.toImmutableSortedMap( - ZonedDateTime::compareTo, // - i -> fromDate.plusMinutes(i * 15), // - i -> existingSchedule[i])); - } - - @Test - public void testInterpolateArrayFloat() { - assertArrayEquals(new double[] { 123, 123, 234, 234, 345 }, // - interpolateArray(new Double[] { null, 123., 234., null, 345., null }), // - 0.0001F); - - assertArrayEquals(new double[] {}, // - interpolateArray(new Double[] { null }), // - 0.0001F); - } - - @Test - public void testInterpolateArrayInteger() { - assertArrayEquals(new int[] { 123, 123, 234, 234, 345 }, // - interpolateArray(new Integer[] { null, 123, 234, null, 345, null })); - - assertArrayEquals(new int[] {}, // - interpolateArray(new Integer[] { null })); - - assertArrayEquals(new int[] { 123, 123 }, // - interpolateArray(new Integer[] { null, 123 })); - - assertArrayEquals(new int[] { 123 }, // - interpolateArray(new Integer[] { 123, null })); - } - - @Test - public void testToPower() { - assertEquals(2000, (int) toPower(500)); - assertNull(toPower(null)); - } - - @Test - public void testGenerateProductionPrediction() { - final var arr = new Integer[] { 1, 2, 3 }; - assertArrayEquals(arr, generateProductionPrediction(arr, 2)); - assertArrayEquals(new Integer[] { 1, 2, 3, 0 }, generateProductionPrediction(arr, 4)); - } - - @Test - public void testJoinConsumptionPredictions() { - assertArrayEquals(// - new Integer[] { 1, 2, 3, 4, 55, 66, 77, 88, 99 }, // - joinConsumptionPredictions(4, // - new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, // - new Integer[] { 11, 22, 33, 44, 55, 66, 77, 88, 99 })); - } - - @Test - public void testFindFirstPeakIndex() { - assertEquals(0, findFirstPeakIndex(0, new double[0])); - assertEquals(0, findFirstPeakIndex(0, new double[] { 1 })); - assertEquals(0, findFirstPeakIndex(0, new double[] { 1, 0 })); - assertEquals(1, findFirstPeakIndex(0, new double[] { 0, 1, 0 })); - assertEquals(1, findFirstPeakIndex(0, new double[] { 0, 1, 0, 1 })); - assertEquals(5, findFirstPeakIndex(5, new double[0])); - } - - @Test - public void testFindFirstValleyIndex() { - assertEquals(0, findFirstValleyIndex(0, new double[0])); - assertEquals(0, findFirstValleyIndex(0, new double[] { 1 })); - assertEquals(1, findFirstValleyIndex(0, new double[] { 1, 0 })); - assertEquals(0, findFirstValleyIndex(0, new double[] { 0, 1, 0 })); - assertEquals(2, findFirstValleyIndex(1, new double[] { 0, 1, 0, 1 })); - assertEquals(5, findFirstValleyIndex(5, new double[0])); - } - - @Test - public void testParamsAreValid() throws Exception { - var builder = Params.create() // - .setTime(TIME) // - .setEssInitialEnergy(0) // - .setEssTotalEnergy(22000) // - .setEssMinSocEnergy(2_000) // - .setEssMaxSocEnergy(20_000) // - .seMaxBuyFromGrid(toEnergy(24_000)) // - .seMaxBuyFromGrid(0) // - .setStates(new StateMachine[0]); - - // No periods are available - assertFalse(paramsAreValid(builder // - .setProductions() // - .setConsumptions() // - .setPrices() // - .build())); - - // Production and Consumption predictions are all zero - assertFalse(paramsAreValid(builder // - .setProductions(0, 0, 0) // - .setConsumptions(0, 0) // - .setPrices(123F) // - .build())); - - // Prices are all the same - assertFalse(paramsAreValid(builder // - .setProductions(0, 1, 3) // - .setConsumptions(0, 2) // - .setPrices(123F, 123F) // - .build())); - - // Finally got it right... - assertTrue(paramsAreValid(builder // - .setProductions(0, 1, 3) // - .setConsumptions(0, 2) // - .setPrices(123F, 124F) // - .build())); - assertEquals(2, builder.build().optimizePeriods().size()); - } - - private static class MyControllerEssLimitTotalDischarge - extends AbstractDummyOpenemsComponent - implements ControllerEssLimitTotalDischarge { - - protected MyControllerEssLimitTotalDischarge(Integer minSoc) { - super("ctrl0", // - OpenemsComponent.ChannelId.values(), // - ControllerEssLimitTotalDischarge.ChannelId.values() // - ); - withValue(this.getMinSocChannel(), minSoc); - } - - @Override - public void run() throws OpenemsNamedException { - } - - @Override - protected MyControllerEssLimitTotalDischarge self() { - return this; - } - } - - private static class MyControllerEssEmergencyCapacityReserve - extends AbstractDummyOpenemsComponent - implements ControllerEssEmergencyCapacityReserve { - - protected MyControllerEssEmergencyCapacityReserve(Integer reserveSoc) { - super("ctrl0", // - OpenemsComponent.ChannelId.values(), // - ControllerEssEmergencyCapacityReserve.ChannelId.values() // - ); - withValue(this.getActualReserveSocChannel(), reserveSoc); - } - - @Override - public void run() throws OpenemsNamedException { - } - - @Override - protected MyControllerEssEmergencyCapacityReserve self() { - return this; - } - } - - @Test - public void testGetEssMinSocEnergy() { - var t1 = new MyControllerEssLimitTotalDischarge(50); - var t2 = new MyControllerEssLimitTotalDischarge(null); - var t3 = new MyControllerEssEmergencyCapacityReserve(30); - assertEquals(5000, getEssMinSocEnergy( - new TimeOfUseTariffControllerImpl.Context(List.of(t3), List.of(t1, t2), null, null, 10000, false), - 10000)); - } - - @Test - public void testHandleScheduleRequest() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-03-04T14:19:00.00Z"), ZoneOffset.UTC); - final var energyScheduler = EnergySchedulerImplTest.create(clock); - - // Simulate historic data - var now = roundDownToQuarter(ZonedDateTime.now(clock)); - final var fromDate = now.minusHours(3); - var timedata = new DummyTimedata("timedata0"); - for (var i = 0; i < 12; i++) { - var quarter = fromDate.plusMinutes(i * 15); - timedata.add(quarter, new ChannelAddress("ctrl0", "QuarterlyPrices"), PAST_HOURLY_PRICES[i]); - timedata.add(quarter, new ChannelAddress("ctrl0", "StateMachine"), PAST_STATES[i]); - timedata.add(quarter, SUM_PRODUCTION, PRODUCTION_PREDICTION_QUARTERLY[i]); - timedata.add(quarter, SUM_CONSUMPTION, CONSUMPTION_PREDICTION_QUARTERLY[i]); - timedata.add(quarter, SUM_ESS_SOC, PAST_SOC[i]); - timedata.add(quarter, SUM_ESS_DISCHARGE_POWER, PRODUCTION_888_20231106[i]); - timedata.add(quarter, SUM_GRID, PRODUCTION_888_20231106[i]); - } - - var optimizer = getOptimizer(energyScheduler); - System.out.println(optimizer); - // TODO this requires a fully configured TimeOfUseTariffControllerImpl - // callCreateParams(optimizer); - // - // // Testing only past data. For full data, optimizer has to be created as - // well. - // var result = handleGetScheduleRequest(optimizer, getNilUuid(), timedata, - // "ctrl0", clock.now()).getResult(); - // - // // JsonUtils.prettyPrint(result); - // - // var schedule = getAsJsonArray(result, "schedule"); - // assertEquals(11, schedule.size()); - // { - // var period = getAsJsonObject(schedule.get(0)); - // assertEquals(PAST_HOURLY_PRICES[0], getAsFloat(period, "price"), 0.00F); - // assertEquals(PRODUCTION_PREDICTION_QUARTERLY[0] / 4, getAsInt(period, - // "production")); - // } - } - @Test public void testCalculateExecutionLimitSeconds() { final var clock = new TimeLeapClock(Instant.parse("2022-01-01T00:00:00.00Z"), ZoneOffset.UTC); @@ -296,42 +36,21 @@ public void testCalculateExecutionLimitSeconds() { } @Test - public void testUpdateSchedule() { - final ZonedDateTime t = ZonedDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")); - final Period pOld = new Period(null, DELAY_DISCHARGE, 0, NO_FLOW); - final Period pNew = new Period(null, BALANCING, 0, NO_FLOW); + public void testSortByScheduler() { + final var scheduler = new DummyScheduler("scheduler0") // + .setControllers("d", "f", null, "b"); + final var list = Stream.of("a", "b", "c", "d", "e") // + .map(id -> new DummyEnergySchedulable(id, null)) // + .toList(); - var schedule = new TreeMap(); - schedule.put(t.minusMinutes(15), pOld); // old entry is removed - schedule.put(t, pOld); // current entry stays - schedule.put(t.plusMinutes(15), pOld); // is overridden - schedule.put(t.plusMinutes(30), pOld); // is overridden - schedule.put(t.plusMinutes(45), pOld); // timestamp is missing in new Schedule -> remove + var result = sortByScheduler(scheduler, list).stream() // + .map(EnergySchedulable::id) // + .toArray(); - var newSchedule = ImmutableSortedMap.naturalOrder() // - .put(t, pNew) // - .put(t.plusMinutes(15), pNew) // - .put(t.plusMinutes(30), pNew) // - .build(); - - updateSchedule(t, schedule, newSchedule); - - // One old entry - assertEquals(1, schedule.values().stream().filter(v -> v == pOld).count()); - - // Two new entries - assertEquals(2, schedule.values().stream().filter(v -> v == pNew).count()); - - // No old entry - assertEquals(0, schedule.keySet().stream().filter(tz -> tz.isBefore(t)).count()); - - // Details - assertEquals(pOld, schedule.get(t)); - assertEquals(pNew, schedule.get(t.plusMinutes(15))); - assertEquals(pNew, schedule.get(t.plusMinutes(30))); - - // No current entry -> handle null - schedule.remove(t); - updateSchedule(t, schedule, newSchedule); + assertArrayEquals(// + new String[] { // + "d", "b", // by Scheduler + "a", "c", "e" // remaining alphabetically + }, result); } } diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/app/AppUtils.java b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/app/AppUtils.java new file mode 100644 index 00000000000..cbfbbdd1508 --- /dev/null +++ b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/app/AppUtils.java @@ -0,0 +1,108 @@ +package io.openems.edge.energy.optimizer.app; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.lang.Double.parseDouble; +import static java.lang.Integer.parseInt; + +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.collect.ImmutableList; + +import io.openems.common.test.TimeLeapClock; +import io.openems.edge.energy.api.EnergyScheduleHandler; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext; +import io.openems.edge.energy.api.simulation.GlobalSimulationsContext.Period; +import io.openems.edge.energy.optimizer.SimulationResult; + +public class AppUtils { + + private AppUtils() { + } + + /** + * Creates {@link GlobalSimulationsContext} from the log output of + * {@link SimulationResult#toLogString()}. + * + * @param log the log output of {@link SimulationResult#toLogString()} + * @param eshs the {@link EnergyScheduleHandler}s + * @return a {@link GlobalSimulationsContext} + */ + public static GlobalSimulationsContext parseGlobalSimulationsContextFromLogString(String log, + ImmutableList eshs) throws IllegalArgumentException { + var headerMatcher = log.lines() // + .filter(l -> l.contains("OPTIMIZER ") && l.contains("GlobalSimulationsContext")) // + .map(l -> applyPattern(HEADER_PATTERN, l)) // + .findFirst().get(); + final var startDateTime = ZonedDateTime.parse(headerMatcher.group("startTime")); + final var clock = new TimeLeapClock(startDateTime.toInstant(), ZoneId.of("UTC")); + + var gridMatcher = applyPattern(GRID_PATTERN, headerMatcher.group("grid")); + final var grid = new GlobalSimulationsContext.Grid(// + parseInt(gridMatcher.group("maxBuy")), // + parseInt(gridMatcher.group("maxSell"))); + + var essMatcher = applyPattern(ESS_PATTERN, headerMatcher.group("ess")); + final var ess = new GlobalSimulationsContext.Ess(// + parseInt(essMatcher.group("currentEnergy")), // + parseInt(essMatcher.group("totalEnergy")), // + parseInt(essMatcher.group("maxChargeEnergy")), // + parseInt(essMatcher.group("maxDischargeEnergy"))); + + var nextTime = new AtomicReference<>(startDateTime); + var periods = log.lines() // + .filter(l -> l.contains("OPTIMIZER ") && !l.contains("GlobalSimulationsContext") && !l.contains("Time")) // + .map(l -> applyPattern(PERIOD_PATTERN, l)) // + .map(m -> { + var time = nextTime.get(); + if (!nextTime.get().toLocalTime().equals(LocalTime.parse(m.group("time"), HOURS_MINUTES))) { + throw new IllegalArgumentException("Times do not match: " + time); + } + nextTime.set(time.plusMinutes(15)); + return (Period) new Period.Quarter(time, // + parseInt(m.group("production")), // + parseInt(m.group("consumption")), // + parseDouble(m.group("price"))); + }) // + .collect(toImmutableList()); + + return new GlobalSimulationsContext(clock, startDateTime, eshs, grid, ess, periods); + } + + private static Matcher applyPattern(Pattern pattern, String line) { + var matcher = pattern.matcher(line); + if (!matcher.find()) { + throw new IllegalArgumentException("Pattern [" + pattern + "] does not match line [" + line + "]"); + } + return matcher; + } + + private static final DateTimeFormatter HOURS_MINUTES = DateTimeFormatter.ofPattern("HH:mm"); + + private static final Pattern HEADER_PATTERN = Pattern.compile("" // + + "startTime=(?\\S*), " // + + "Grid\\[(?.*)\\], " // + + "Ess\\[(?.*)\\]"); + + private static final Pattern GRID_PATTERN = Pattern.compile("" // + + "maxBuy=(?\\d+), " // + + "maxSell=(?\\d+)"); + + private static final Pattern ESS_PATTERN = Pattern.compile("" // + + "currentEnergy=(?\\d+), " // + + "totalEnergy=(?\\d+), " // + + "maxChargeEnergy=(?\\d+), " // + + "maxDischargeEnergy=(?\\d+)"); + + private static final Pattern PERIOD_PATTERN = Pattern.compile("" // + + "(?
  • F = Failure * */ - RAW_CHARGE_STATUS_CHARGEPOINT(Doc.of(OpenemsType.STRING), "secc", "port0", "ci", "charge", "cp", "status"), // + RAW_CHARGE_STATUS_CHARGEPOINT(Doc.of(STRING), // + "secc", "port0", "ci", "charge", "cp", "status"), // // SALIA - RAW_SALIA_CHARGE_MODE(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "chargemode"), // - RAW_SALIA_CHANGE_METER(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "changemeter"), // - RAW_SALIA_AUTHMODE(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "authmode"), // - RAW_SALIA_FIRMWARESTATE(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "firmwarestate"), // - RAW_SALIA_FIRMWAREPROGRESS(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "firmwareprogress"), // - RAW_SALIA_PUBLISH(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "publish"), // + RAW_SALIA_CHARGE_MODE(Doc.of(STRING), // + "secc", "port0", "salia", "chargemode"), // + RAW_SALIA_CHANGE_METER(Doc.of(STRING), // + "secc", "port0", "salia", "changemeter"), // + RAW_SALIA_AUTHMODE(Doc.of(STRING), // + "secc", "port0", "salia", "authmode"), // + RAW_SALIA_FIRMWARESTATE(Doc.of(STRING), // + "secc", "port0", "salia", "firmwarestate"), // + RAW_SALIA_FIRMWAREPROGRESS(Doc.of(STRING), // + "secc", "port0", "salia", "firmwareprogress"), // + RAW_SALIA_PUBLISH(Doc.of(STRING), // + "secc", "port0", "salia", "publish"), // // SESSION - RAW_SESSION_STATUS_AUTHORIZATION(Doc.of(OpenemsType.STRING), "secc", "port0", "session", - "authorization_status"), // - RAW_SESSION_SLAC_STARTED(Doc.of(OpenemsType.STRING), "secc", "port0", "session", "slac_started"), // - RAW_SESSION_AUTHORIZATION_METHOD(Doc.of(OpenemsType.STRING), "secc", "port0", "session", - "authorization_method"), // + RAW_SESSION_STATUS_AUTHORIZATION(Doc.of(STRING), // + "secc", "port0", "session", "authorization_status"), // + RAW_SESSION_SLAC_STARTED(Doc.of(STRING), // + "secc", "port0", "session", "slac_started"), // + RAW_SESSION_AUTHORIZATION_METHOD(Doc.of(STRING), // + "secc", "port0", "session", "authorization_method"), // // CONTACTOR - RAW_CONTACTOR_HLC_TARGET(Doc.of(OpenemsType.STRING), "secc", "port0", "contactor", "state", "hlc_target"), // - RAW_CONTACTOR_ACTUAL(Doc.of(OpenemsType.STRING), "secc", "port0", "contactor", "state", "actual"), // - RAW_CONTACTOR_TARGET(Doc.of(OpenemsType.STRING), "secc", "port0", "contactor", "state", "target"), // - RAW_CONTACTOR_ERROR(Doc.of(OpenemsType.STRING), "secc", "port0", "contactor", "error"), // + RAW_CONTACTOR_HLC_TARGET(Doc.of(STRING), // + "secc", "port0", "contactor", "state", "hlc_target"), // + RAW_CONTACTOR_ACTUAL(Doc.of(STRING), // + "secc", "port0", "contactor", "state", "actual"), // + RAW_CONTACTOR_TARGET(Doc.of(STRING), // + "secc", "port0", "contactor", "state", "target"), // + RAW_CONTACTOR_ERROR(Doc.of(STRING), // + "secc", "port0", "contactor", "error"), // // METERING - METER - RAW_METER_SERIALNUMBER(Doc.of(OpenemsType.STRING), "secc", "port0", "metering", "meter", "serialnumber"), // - RAW_METER_TYPE(Doc.of(OpenemsType.STRING), "secc", "port0", "metering", "meter", "type"), // - METER_NOT_AVAILABLE(Doc.of(Level.INFO) // - .text("No meter values available. The communication cable of the internal meter may be loose.")), // + RAW_METER_SERIALNUMBER(Doc.of(STRING), // + "secc", "port0", "metering", "meter", "serialnumber"), // + RAW_METER_TYPE(Doc.of(STRING), // + "secc", "port0", "metering", "meter", "type"), // + METER_NOT_AVAILABLE(Doc.of(WARNING) // + .translationKey(EvcsHardyBarth.class, "noMeterAvailable")), // RAW_METER_AVAILABLE(new BooleanDoc()// .onChannelSetNextValue((hardyBarth, value) -> { var notAvailable = value.get() == null ? null : !value.get(); hardyBarth.channel(EvcsHardyBarth.ChannelId.METER_NOT_AVAILABLE).setNextValue(notAvailable); - }), "secc", "port0", "metering", "meter", "available"), // - - // METERING - POWER - RAW_ACTIVE_POWER_L1(Doc.of(OpenemsType.LONG).unit(Unit.WATT), value -> { - Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, value); - return TypeUtils.getAsType(OpenemsType.LONG, TypeUtils.multiply(doubleValue, SCALE_FACTOR_MINUS_1)); - }, "secc", "port0", "metering", "power", "active", "ac", "l1", "actual"), // - - RAW_ACTIVE_POWER_L2(Doc.of(OpenemsType.LONG).unit(Unit.WATT), value -> { - Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, value); - return TypeUtils.getAsType(OpenemsType.LONG, TypeUtils.multiply(doubleValue, SCALE_FACTOR_MINUS_1)); - }, "secc", "port0", "metering", "power", "active", "ac", "l2", "actual"), // - - RAW_ACTIVE_POWER_L3(Doc.of(OpenemsType.LONG).unit(Unit.WATT), value -> { - Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, value); - return TypeUtils.getAsType(OpenemsType.LONG, TypeUtils.multiply(doubleValue, SCALE_FACTOR_MINUS_1)); - }, "secc", "port0", "metering", "power", "active", "ac", "l3", "actual"), // - - // METERING - CURRENT - RAW_ACTIVE_CURRENT_L1(Doc.of(OpenemsType.LONG).unit(Unit.MILLIAMPERE), "secc", "port0", "metering", "current", - "ac", "l1", "actual"), // - RAW_ACTIVE_CURRENT_L2(Doc.of(OpenemsType.LONG).unit(Unit.MILLIAMPERE), "secc", "port0", "metering", "current", - "ac", "l2", "actual"), // - RAW_ACTIVE_CURRENT_L3(Doc.of(OpenemsType.LONG).unit(Unit.MILLIAMPERE), "secc", "port0", "metering", "current", - "ac", "l3", "actual"), // + }), // + "secc", "port0", "metering", "meter", "available"), // // METERING - ENERGY - RAW_ACTIVE_ENERGY_TOTAL(Doc.of(OpenemsType.DOUBLE).unit(Unit.CUMULATED_WATT_HOURS), "secc", "port0", "metering", - "energy", "active_total", "actual"), // - RAW_ACTIVE_ENERGY_EXPORT(Doc.of(OpenemsType.DOUBLE).unit(Unit.CUMULATED_WATT_HOURS), "secc", "port0", - "metering", "energy", "active_export", "actual"), // + RAW_ACTIVE_ENERGY_TOTAL(Doc.of(DOUBLE) // + .unit(CUMULATED_WATT_HOURS), // + "secc", "port0", "metering", "energy", "active_total", "actual"), // + RAW_ACTIVE_ENERGY_EXPORT(Doc.of(DOUBLE) // + .unit(CUMULATED_WATT_HOURS), // + "secc", "port0", "metering", "energy", "active_export", "actual"), // // EMERGENCY SHUTDOWN - RAW_EMERGENCY_SHUTDOWN(Doc.of(OpenemsType.STRING), "secc", "port0", "emergency_shutdown"), // + RAW_EMERGENCY_SHUTDOWN(Doc.of(STRING), // + "secc", "port0", "emergency_shutdown"), // // RCD - RAW_RCD_AVAILABLE(Doc.of(OpenemsType.BOOLEAN), "secc", "port0", "rcd", "recloser", "available"), // + RAW_RCD_AVAILABLE(Doc.of(BOOLEAN), // + "secc", "port0", "rcd", "recloser", "available"), // // PLUG LOCK - RAW_PLUG_LOCK_STATE_ACTUAL(Doc.of(OpenemsType.STRING), "secc", "port0", "plug_lock", "state", "actual"), // - RAW_PLUG_LOCK_STATE_TARGET(Doc.of(OpenemsType.STRING), "secc", "port0", "plug_lock", "state", "target"), // - RAW_PLUG_LOCK_ERROR(Doc.of(OpenemsType.STRING), "secc", "port0", "plug_lock", "error"), // + RAW_PLUG_LOCK_STATE_ACTUAL(Doc.of(STRING), // + "secc", "port0", "plug_lock", "state", "actual"), // + RAW_PLUG_LOCK_STATE_TARGET(Doc.of(STRING), // + "secc", "port0", "plug_lock", "state", "target"), // + RAW_PLUG_LOCK_ERROR(Doc.of(STRING), // + "secc", "port0", "plug_lock", "error"), // // CHARGE POINT - RAW_CP_STATE(Doc.of(OpenemsType.STRING), "secc", "port0", "cp", "state"), // + RAW_CP_STATE(Doc.of(STRING), // + "secc", "port0", "cp", "state"), // // DIODE PRESENT - RAW_DIODE_PRESENT(Doc.of(OpenemsType.STRING), "secc", "port0", "diode_present"), // + RAW_DIODE_PRESENT(Doc.of(STRING), // + "secc", "port0", "diode_present"), // // CABLE CURRENT LIMIT - RAW_CABLE_CURRENT_LIMIT(Doc.of(OpenemsType.STRING), "secc", "port0", "cable_current_limit"), // + RAW_CABLE_CURRENT_LIMIT(Doc.of(STRING), // + "secc", "port0", "cable_current_limit"), // // VENTILATION - RAW_VENTILATION_STATE_ACTUAL(Doc.of(OpenemsType.STRING), "secc", "port0", "ventilation", "state", "actual"), // - RAW_VENTILATION_STATE_TARGET(Doc.of(OpenemsType.STRING), "secc", "port0", "ventilation", "state", "target"), // - RAW_VENTILATION_AVAILABLE(Doc.of(OpenemsType.BOOLEAN), "secc", "port0", "ventilation", "available"), // + RAW_VENTILATION_STATE_ACTUAL(Doc.of(STRING), // + "secc", "port0", "ventilation", "state", "actual"), // + RAW_VENTILATION_STATE_TARGET(Doc.of(STRING), // + "secc", "port0", "ventilation", "state", "target"), // + RAW_VENTILATION_AVAILABLE(Doc.of(BOOLEAN), // + "secc", "port0", "ventilation", "available"), // // EV - PRESENT - RAW_EV_PRESENT(Doc.of(OpenemsType.STRING), "secc", "port0", "ev_present"), // + RAW_EV_PRESENT(Doc.of(STRING), // + "secc", "port0", "ev_present"), // // CHARGING - RAW_CHARGING(Doc.of(OpenemsType.STRING), "secc", "port0", "charging"), // + RAW_CHARGING(Doc.of(STRING), // + "secc", "port0", "charging"), // // RFID - RAW_RFID_AUTHORIZEREQ(Doc.of(OpenemsType.STRING), "secc", "port0", "rfid", "authorizereq"), // - RAW_RFID_AVAILABLE(Doc.of(OpenemsType.BOOLEAN), "secc", "port0", "rfid", "available"), // + RAW_RFID_AUTHORIZEREQ(Doc.of(STRING), // + "secc", "port0", "rfid", "authorizereq"), // + RAW_RFID_AVAILABLE(Doc.of(BOOLEAN), // + "secc", "port0", "rfid", "available"), // // GRID CURRENT LIMIT - RAW_GRID_CURRENT_LIMIT(Doc.of(OpenemsType.STRING), "secc", "port0", "grid_current_limit"), // + RAW_GRID_CURRENT_LIMIT(Doc.of(STRING), // + "secc", "port0", "grid_current_limit"), // // SLAC ERROR - RAW_SLAC_ERROR(Doc.of(OpenemsType.STRING), "secc", "port0", "slac_error"), // + RAW_SLAC_ERROR(Doc.of(STRING), // + "secc", "port0", "slac_error"), // // DEVICE - RAW_DEVICE_PRODUCT(Doc.of(OpenemsType.STRING), "device", "product"), // - RAW_DEVICE_MODELNAME(Doc.of(OpenemsType.STRING), "device", "modelname"), // - RAW_DEVICE_HARDWARE_VERSION(Doc.of(OpenemsType.STRING), "device", "hardware_version"), // - RAW_DEVICE_SOFTWARE_VERSION(Doc.of(OpenemsType.STRING), "device", "software_version"), // - RAW_DEVICE_VCS_VERSION(Doc.of(OpenemsType.STRING), "device", "vcs_version"), // - RAW_DEVICE_HOSTNAME(Doc.of(OpenemsType.STRING), "device", "hostname"), // - RAW_DEVICE_MAC_ADDRESS(Doc.of(OpenemsType.STRING), "device", "mac_address"), // - RAW_DEVICE_SERIAL(Doc.of(OpenemsType.LONG), "device", "serial"), // - RAW_DEVICE_UUID(Doc.of(OpenemsType.STRING), "device", "uuid"), // + RAW_DEVICE_PRODUCT(Doc.of(STRING), // + "device", "product"), // + RAW_DEVICE_MODELNAME(Doc.of(STRING), // + "device", "modelname"), // + RAW_DEVICE_HARDWARE_VERSION(Doc.of(STRING), // + "device", "hardware_version"), // + RAW_DEVICE_SOFTWARE_VERSION(Doc.of(STRING), // + "device", "software_version"), // + RAW_DEVICE_VCS_VERSION(Doc.of(STRING), // + "device", "vcs_version"), // + RAW_DEVICE_HOSTNAME(Doc.of(STRING), // + "device", "hostname"), // + RAW_DEVICE_MAC_ADDRESS(Doc.of(STRING), // + "device", "mac_address"), // + RAW_DEVICE_SERIAL(Doc.of(LONG), // + "device", "serial"), // + RAW_DEVICE_UUID(Doc.of(STRING), // + "device", "uuid"), // ; private final Doc doc; diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImpl.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImpl.java index 3b77f4df677..87abcc6cfb0 100644 --- a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImpl.java +++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImpl.java @@ -1,6 +1,17 @@ package io.openems.edge.evcs.hardybarth; +import static io.openems.edge.bridge.http.api.BridgeHttp.DEFAULT_CONNECT_TIMEOUT; +import static io.openems.edge.bridge.http.api.BridgeHttp.DEFAULT_READ_TIMEOUT; +import static io.openems.edge.bridge.http.api.HttpMethod.GET; +import static io.openems.edge.bridge.http.api.HttpMethod.PUT; +import static io.openems.edge.evcs.api.ChargingType.AC; +import static io.openems.edge.evcs.api.Phases.THREE_PHASE; +import static java.lang.Math.round; +import static java.util.Collections.emptyMap; + import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -19,16 +30,23 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; -import io.openems.common.utils.JsonUtils; +import io.openems.common.types.HttpStatus; +import io.openems.common.types.MeterType; +import io.openems.edge.bridge.http.api.BridgeHttp; +import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; +import io.openems.edge.bridge.http.api.BridgeHttpFactory; +import io.openems.edge.bridge.http.api.HttpMethod; +import io.openems.edge.bridge.http.api.HttpResponse; import io.openems.edge.common.channel.StringReadChannel; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.evcs.api.AbstractManagedEvcsComponent; -import io.openems.edge.evcs.api.ChargingType; +import io.openems.edge.evcs.api.DeprecatedEvcs; import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.EvcsPower; import io.openems.edge.evcs.api.ManagedEvcs; -import io.openems.edge.evcs.api.Phases; +import io.openems.edge.evcs.api.WriteHandler; +import io.openems.edge.meter.api.ElectricityMeter; @Designate(ocd = Config.class, factory = true) @Component(// @@ -37,82 +55,118 @@ configurationPolicy = ConfigurationPolicy.REQUIRE // ) @EventTopics({ // - EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, // EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, // }) public class EvcsHardyBarthImpl extends AbstractManagedEvcsComponent - implements OpenemsComponent, EventHandler, EvcsHardyBarth, Evcs, ManagedEvcs { + implements OpenemsComponent, EventHandler, EvcsHardyBarth, Evcs, ManagedEvcs, DeprecatedEvcs, ElectricityMeter { - protected final Logger log = LoggerFactory.getLogger(EvcsHardyBarthImpl.class); + private final Logger log = LoggerFactory.getLogger(EvcsHardyBarthImpl.class); - @Reference - private EvcsPower evcsPower; + protected final HardyBarthReadUtils readUtils = new HardyBarthReadUtils(this); - /** API for main REST API functions. */ - protected HardyBarthApi api; - /** ReadWorker and WriteHandler: Reading and sending data to the EVCS. */ - private final HardyBarthReadWorker readWorker = new HardyBarthReadWorker(this); /** * Master EVCS is responsible for RFID authentication (Not implemented for now). */ protected boolean masterEvcs = true; - protected Config config; + private BridgeHttp httpBridge; + private Config config; + + @Reference + private EvcsPower evcsPower; + + @Reference + private BridgeHttpFactory httpBridgeFactory; public EvcsHardyBarthImpl() { super(// OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // Evcs.ChannelId.values(), // ManagedEvcs.ChannelId.values(), // - EvcsHardyBarth.ChannelId.values() // + EvcsHardyBarth.ChannelId.values(), // + DeprecatedEvcs.ChannelId.values() // ); + DeprecatedEvcs.copyToDeprecatedEvcsChannels(this); + ElectricityMeter.calculateSumCurrentFromPhases(this); + ElectricityMeter.calculateAverageVoltageFromPhases(this); } @Activate private void activate(ComponentContext context, Config config) { super.activate(context, config.id(), config.alias(), config.enabled()); + // TODO stop here if not enabled this.config = config; - this._setChargingType(ChargingType.AC); + this._setChargingType(AC); this._setFixedMinimumHardwarePower(config.minHwCurrent() / 1000 * 3 * 230); this._setFixedMaximumHardwarePower(config.maxHwCurrent() / 1000 * 3 * 230); this._setPowerPrecision(230); - this._setPhases(Phases.THREE_PHASE); - - if (config.enabled()) { - this.api = new HardyBarthApi(config.ip(), this); + this._setPhases(THREE_PHASE); - // Reading the given values - this.readWorker.activate(config.id()); - this.readWorker.triggerNextRun(); - } + this.httpBridge = this.httpBridgeFactory.get(); + // formerly .setHeartbeat + // The internal heartbeat is currently too fast - it is not enough to write + // every second by default. We have to disable it to run the evcs + // properly. + // TODO: The manufacturer must be asked if it is possible to read the heartbeat + // status so that we can check if the heartbeat is really disabled and if the + // heartbeat time can be increased to be able to use this feature. + this.httpBridge.subscribeCycle(1, // + this.createEndpoint(PUT, "/api/secc", "{\"salia/heartbeat\":\"off\"}"), // + t -> this._setChargingstationCommunicationFailed(false), + t -> this._setChargingstationCommunicationFailed(true)); + this.httpBridge.subscribeCycle(1, // + this.createEndpoint(GET, "/api", null), // + t -> { + this.readUtils.handleGetApiCallResponse(t, config.phaseRotation()); + this._setChargingstationCommunicationFailed(false); + }, // + t -> this._setChargingstationCommunicationFailed(true)); } @Override @Deactivate protected void deactivate() { super.deactivate(); + this.httpBridgeFactory.unget(this.httpBridge); + this.httpBridge = null; + } - if (this.readWorker != null) { - this.readWorker.deactivate(); - } + @Override + public MeterType getMeterType() { + return MeterType.MANAGED_CONSUMPTION_METERED; + } + + private Endpoint getTargetEndpoint(int target) { + return this.createEndpoint(PUT, "/api/secc", "{\"salia/pausecharging\":\"" + target + "\"}"); + } + + private Endpoint createEndpoint(HttpMethod httpMethod, String url, String body) { + return createEndpoint(this.config.ip(), httpMethod, url, body); + } + + protected static Endpoint createEndpoint(String ip, HttpMethod httpMethod, String url, String body) { + return new Endpoint(// + new StringBuilder("http://").append(ip).append(url).toString(), // + httpMethod, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, // + body, // + emptyMap()); } @Override - public void handleEvent(Event event) { + protected WriteHandler createWriteHandler() { + return new HardyBarthWriteHandler(this); + } + @Override + public void handleEvent(Event event) { if (!this.isEnabled()) { return; } super.handleEvent(event); switch (event.getTopic()) { - case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: - - this.setManualMode(); - this.setHeartbeat(); - this.readWorker.triggerNextRun(); - - // TODO: intelligent firmware update - break; + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // + -> this.setManualMode(); } } @@ -125,39 +179,14 @@ public void handleEvent(Event event) { private void setManualMode() { StringReadChannel channelChargeMode = this.channel(EvcsHardyBarth.ChannelId.RAW_SALIA_CHARGE_MODE); Optional valueOpt = channelChargeMode.value().asOptional(); - if (valueOpt.isPresent()) { - if (!valueOpt.get().equals("manual")) { - // Set to manual mode - try { - this.debugLog("Setting HardyBarth to manual chargemode"); - JsonElement result = this.api.sendPutRequest("/api/secc", "salia/chargemode", "manual"); - this.debugLog(result.toString()); - } catch (OpenemsNamedException e) { - e.printStackTrace(); - } - } + if (valueOpt.map(t -> !t.equals("manual")).orElse(true)) { + return; } - } - - /** - * Set heartbeat. - * - *

    - * Sets the heartbeat to on or off. - */ - private void setHeartbeat() { - // The internal heartbeat is currently too fast - it is not enough to write - // every second by default. We have to disable it to run the evcs - // properly. - // TODO: The manufacturer must be asked if it is possible to read the heartbeat - // status so that we can check if the heartbeat is really disabled and if the - // heartbeat time can be increased to be able to use this feature. + this.debugLog("Setting HardyBarth to manual chargemode"); + this.httpBridge // + .requestJson(this.createEndpoint(PUT, "/api/secc", "{\"salia/chargemode\":\"manual\"}")) // + .thenAccept(t -> this.debugLog(t.toString())); - try { - this.api.sendPutRequest("/api/secc", "salia/heartbeat", "off"); - } catch (OpenemsNamedException e) { - e.printStackTrace(); - } } /** @@ -219,7 +248,6 @@ public boolean getConfiguredDebugMode() { @Override public boolean applyChargePowerLimit(int power) throws OpenemsNamedException { - // TODO: Use power precision to set valid power if it is used in UI part too // e.g. int precision = TypeUtils.getAsType(OpenemsType.INTEGER, // this.getPowerPrecision().orElse(230d)); @@ -227,7 +255,7 @@ public boolean applyChargePowerLimit(int power) throws OpenemsNamedException { // Convert it to ampere and apply hard limits int phases = this.getPhasesAsInt(); - Integer current = (int) Math.round(power / (double) phases / 230.0); + final var current = round(power / (float) phases / 230.F); return this.setTarget(current); } @@ -244,25 +272,25 @@ public boolean pauseChargeProcess() throws OpenemsNamedException { * @return boolean if the target was set * @throws OpenemsNamedException on error */ - private boolean setTarget(int current) throws OpenemsNamedException { - - JsonElement resultPause; + private boolean setTarget(int current) { + CompletableFuture> resultPause = null; if (current > 0) { // Send stop pause request - resultPause = this.api.sendPutRequest("/api/secc", "salia/pausecharging", "" + 0); + resultPause = this.httpBridge.requestJson(this.getTargetEndpoint(0)); } else { - // Send pause charging request - resultPause = this.api.sendPutRequest("/api/secc", "salia/pausecharging", "" + 1); + resultPause = this.httpBridge.requestJson(this.getTargetEndpoint(1)); this.debugLog("Setting HardyBarth " + this.alias() + " to pause"); } // Send charge power limit - JsonElement resultLimit = this.api.sendPutRequest("/api/secc", "grid_current_limit", "" + current); - - Optional resultLimitVal = JsonUtils.getAsOptionalString(resultLimit, "result"); - Optional resultPauseVal = JsonUtils.getAsOptionalString(resultPause, "result"); - - return resultLimitVal.orElse("").equals("ok") && resultPauseVal.orElse("").equals("ok"); + final var resultLimit = this.httpBridge.requestJson( + this.createEndpoint(PUT, "/api/secc", "{\"" + "grid_current_limit" + "\":\"" + current + "\"}")); + try { + return resultLimit.get().status().equals(HttpStatus.OK) && resultPause.get().status().equals(HttpStatus.OK); + } catch (InterruptedException | ExecutionException e) { + this.log.error("Unable to set EVCS Target"); + return false; + } } @Override @@ -277,12 +305,12 @@ public int getMinimumTimeTillChargingLimitTaken() { @Override public int getConfiguredMinimumHardwarePower() { - return Math.round(this.config.minHwCurrent() / 1000f) * DEFAULT_VOLTAGE * Phases.THREE_PHASE.getValue(); + return round(this.config.minHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue(); } @Override public int getConfiguredMaximumHardwarePower() { - return Math.round(this.config.maxHwCurrent() / 1000f) * DEFAULT_VOLTAGE * Phases.THREE_PHASE.getValue(); + return round(this.config.maxHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue(); } @Override diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthApi.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthApi.java deleted file mode 100644 index 4b088d2d5a3..00000000000 --- a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthApi.java +++ /dev/null @@ -1,154 +0,0 @@ -package io.openems.edge.evcs.hardybarth; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.net.HttpURLConnection; -import java.net.URL; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.utils.JsonUtils; - -/** - * Implements the Hardy Barth Api. - */ -public class HardyBarthApi { - - private final String baseUrl; - private final String authorizationHeader; - private final EvcsHardyBarthImpl hardyBarthImpl; - - public HardyBarthApi(String ip, EvcsHardyBarthImpl hardyBarthImpl) { - this.baseUrl = "http://" + ip; - this.authorizationHeader = "Basic "; - this.hardyBarthImpl = hardyBarthImpl; - } - - /** - * Sends a get request to the Hardy Barth. - * - * @param endpoint the REST Api endpoint - * @return a JsonObject or JsonArray - * @throws OpenemsNamedException on error - */ - public JsonElement sendGetRequest(String endpoint) throws OpenemsNamedException { - var putRequestFailed = false; - JsonObject result = null; - - try { - // Create URL like "http://192.168.8.101/api/" - var url = new URL(this.baseUrl + endpoint); - - // Open http url connection - var con = (HttpURLConnection) url.openConnection(); - - // Set general information - con.setRequestProperty("Authorization", this.authorizationHeader); - con.setRequestMethod("GET"); - con.setConnectTimeout(5000); - con.setReadTimeout(5000); - - // Read response - String body; - try (var in = new BufferedReader(new InputStreamReader(con.getInputStream()))) { - - // Read HTTP response - var content = new StringBuilder(); - String line; - while ((line = in.readLine()) != null) { - content.append(line); - content.append(System.lineSeparator()); - } - body = content.toString(); - } - - // Get response code - var status = con.getResponseCode(); - if (status >= 300) { - putRequestFailed = true; - throw new OpenemsException( - "Error while reading from Hardy Barth API. Response code: " + status + ". " + body); - } - putRequestFailed = false; - // Parse response to JSON - result = JsonUtils.parseToJsonObject(body); - } catch (OpenemsNamedException | IOException e) { - putRequestFailed = true; - } - - // Set state and return result - this.hardyBarthImpl._setChargingstationCommunicationFailed(putRequestFailed); - return result; - } - - /** - * Sends a get request to the Hardy Barth. - * - * @param endpoint the REST Api endpoint @return a JsonObject or - * JsonArray @throws OpenemsNamedException on error @throws - * @param key The key in the properties - * @param value The value of the key property - * @return A JsonObject - * @throws OpenemsNamedException on error - */ - public JsonObject sendPutRequest(String endpoint, String key, String value) throws OpenemsNamedException { - var putRequestFailed = false; - JsonObject result = null; - - try { - // Create URL like "http://192.168.8.101/api/" - var url = new URL(this.baseUrl + endpoint); - - // Open http url connection - var connection = (HttpURLConnection) url.openConnection(); - - // Set general information - connection.setRequestProperty("Authorization", this.authorizationHeader); - connection.setRequestMethod("PUT"); - connection.setDoOutput(true); - connection.setConnectTimeout(5000); - connection.setReadTimeout(5000); - - // Write "topic" and "value" on request properties - var osw = new OutputStreamWriter(connection.getOutputStream()); - osw.write("{\"" + key + "\":\"" + value + "\"}"); - osw.flush(); - osw.close(); - - String body; - try (var in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { - - // Read HTTP response - var content = new StringBuilder(); - String line; - while ((line = in.readLine()) != null) { - content.append(line); - content.append(System.lineSeparator()); - } - body = content.toString(); - } - - // Get response code - var status = connection.getResponseCode(); - if ((status >= 300) && (status >= 0)) { - // Respond error status-code - putRequestFailed = true; - throw new OpenemsException( - "Error while reading from Hardy Barth API. Response code: " + status + ". " + body); - } - // Result OK - result = JsonUtils.parseToJsonObject(body); - } catch (IOException e) { - putRequestFailed = true; - } - - // Set state and return result - this.hardyBarthImpl._setChargingstationCommunicationFailed(putRequestFailed); - return result; - } -} diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadUtils.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadUtils.java new file mode 100644 index 00000000000..d20a8a44d04 --- /dev/null +++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadUtils.java @@ -0,0 +1,272 @@ +package io.openems.edge.evcs.hardybarth; + +import static io.openems.common.types.OpenemsType.FLOAT; +import static io.openems.common.types.OpenemsType.INTEGER; +import static io.openems.common.types.OpenemsType.LONG; +import static io.openems.common.types.OpenemsType.STRING; +import static io.openems.common.utils.JsonUtils.parseToJsonObject; +import static io.openems.edge.evcs.api.Evcs.evaluatePhaseCount; +import static io.openems.edge.evcs.hardybarth.EvcsHardyBarth.SCALE_FACTOR_MINUS_1; +import static java.lang.Math.round; + +import java.util.Optional; +import java.util.function.Function; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.OpenemsType; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.bridge.http.api.HttpResponse; +import io.openems.edge.common.type.TypeUtils; +import io.openems.edge.evcs.api.PhaseRotation; +import io.openems.edge.evcs.api.PhaseRotation.RotatedPhases; +import io.openems.edge.evcs.api.Status; + +public class HardyBarthReadUtils { + private final EvcsHardyBarthImpl parent; + + private int chargingFinishedCounter = 0; + private int errorCounter = 0; + + public HardyBarthReadUtils(EvcsHardyBarthImpl parent) { + this.parent = parent; + } + + /** + * Set the value for every Evcs.ChannelId. + * + * @param json given raw data in JSON + * @param phaseRotation the configured {@link PhaseRotation} + */ + protected void setEvcsChannelIds(JsonElement json, PhaseRotation phaseRotation) { + final var hb = this.parent; + + // Energy + hb._setEnergySession(getValueFromJson(STRING, json, // + value -> { + if (value == null) { + return null; + } + var chargedata = TypeUtils.getAsType(STRING, value).split("\\|"); + if (chargedata.length == 3) { + return round(TypeUtils.getAsType(FLOAT, chargedata[2]) * 1000); + } + return null; + }, "secc", "port0", "salia", "chargedata")); + hb._setActiveProductionEnergy(getValueFromJson(LONG, json, // + value -> TypeUtils.getAsType(LONG, value), // + "secc", "port0", "metering", "energy", "active_import", "actual")); + + // Current + final var currentL1 = getAsIntOrElse(json, 0, "secc", "port0", "metering", "current", "ac", "l1", "actual"); + final var currentL2 = getAsIntOrElse(json, 0, "secc", "port0", "metering", "current", "ac", "l2", "actual"); + final var currentL3 = getAsIntOrElse(json, 0, "secc", "port0", "metering", "current", "ac", "l3", "actual"); + + // Power + final var activePowerL1 = getAsInteger(json, SCALE_FACTOR_MINUS_1, // + "secc", "port0", "metering", "power", "active", "ac", "l1", "actual"); + final var activePowerL2 = getAsInteger(json, SCALE_FACTOR_MINUS_1, // + "secc", "port0", "metering", "power", "active", "ac", "l2", "actual"); + final var activePowerL3 = getAsInteger(json, SCALE_FACTOR_MINUS_1, // + "secc", "port0", "metering", "power", "active", "ac", "l3", "actual"); + + // Voltage + final var voltageL1 = activePowerL1 == null ? null : round(activePowerL1 * 1_000_000F / currentL1); + final var voltageL2 = activePowerL2 == null ? null : round(activePowerL2 * 1_000_000F / currentL2); + final var voltageL3 = activePowerL3 == null ? null : round(activePowerL3 * 1_000_000F / currentL3); + + var rp = RotatedPhases.from(phaseRotation, // + voltageL1, currentL1, activePowerL1, // + voltageL2, currentL2, activePowerL2, // + voltageL3, currentL3, activePowerL3); + hb._setVoltageL1(rp.voltageL1()); + hb._setVoltageL2(rp.voltageL2()); + hb._setVoltageL3(rp.voltageL3()); + hb._setCurrentL1(rp.currentL1()); + hb._setCurrentL2(rp.currentL2()); + hb._setCurrentL3(rp.currentL3()); + hb._setActivePowerL1(rp.activePowerL1()); + hb._setActivePowerL2(rp.activePowerL2()); + hb._setActivePowerL3(rp.activePowerL3()); + + // Phases: keep last value if no power value was given + var phases = evaluatePhaseCount(rp.activePowerL1(), rp.activePowerL2(), rp.activePowerL3()); + if (phases != null) { + hb._setPhases(phases); + this.parent.debugLog("Used phases: " + phases); + } + + // ACTIVE_POWER + final var activePower = Optional.ofNullable(getAsInteger(json, SCALE_FACTOR_MINUS_1, // + "secc", "port0", "metering", "power", "active_total", "actual")) // + .map(p -> p < 100 ? 0 : p) // Ignore the consumption of the charger itself + .orElse(null); + this.parent._setActivePower(activePower); + + // STATUS + var status = getValueFromJson(STRING, json, value -> { + var stringValue = TypeUtils.getAsType(STRING, value); + if (stringValue == null) { + this.errorCounter++; + this.parent.debugLog("Hardy Barth RAW_STATUS would be null! Raw value: " + value); + if (this.errorCounter > 3) { + return Status.ERROR; + } + return this.parent.getStatus(); + } + + Status rawStatus = switch (stringValue) { + case "A" -> Status.NOT_READY_FOR_CHARGING; + case "B" -> { + var tmpStatus = Status.READY_FOR_CHARGING; + + // Detect if the car is full + if (this.parent.getSetChargePowerLimit().orElse(0) >= this.parent.getMinimumHardwarePower().orElse(0) + && activePower <= 0) { + + if (this.chargingFinishedCounter >= 90) { + tmpStatus = Status.CHARGING_FINISHED; + } else { + this.chargingFinishedCounter++; + } + } else { + this.chargingFinishedCounter = 0; + + // Charging rejected because we are forcing to pause charging + if (this.parent.getSetChargePowerLimit().orElse(0) == 0) { + tmpStatus = Status.CHARGING_REJECTED; + } + } + yield tmpStatus; + } + case "C", "D" -> Status.CHARGING; + case "E", "F" -> { + this.errorCounter++; + this.parent.debugLog("Hardy Barth RAW_STATUS would be an error! Raw value: " + stringValue + + " - Error counter: " + this.errorCounter); + if (this.errorCounter > 3) { + yield Status.ERROR; + } + yield this.parent.getStatus(); + } + default -> { + this.parent.debugLog("State " + stringValue + " is not a valid state"); + yield Status.UNDEFINED; + } + }; + + if (!stringValue.equals("B")) { + this.chargingFinishedCounter = 0; + } + if (!stringValue.equals("E") || !stringValue.equals("F")) { + this.errorCounter = 0; + } + + return rawStatus; + }, "secc", "port0", "ci", "charge", "cp", "status"); + + this.parent._setStatus(status); + } + + private static Integer getAsInteger(JsonElement json, float scaleFactor, String... jsonPaths) { + return getValueFromJson(INTEGER, json, // + value -> value == null ? null // + : round(TypeUtils.getAsType(INTEGER, value) * scaleFactor), // + jsonPaths); + } + + private static int getAsIntOrElse(JsonElement json, int orElse, String... jsonPaths) { + var result = getValueFromJson(INTEGER, json, // + value -> TypeUtils.getAsType(INTEGER, value), // + jsonPaths); + return result == null // + ? orElse // + : result; + } + + /** + * Get the last JSON element and it's value, by running through the given + * jsonPath. + * + * @param openemsType the {@link OpenemsType}s + * @param json Raw JsonElement. + * @param converter Converter, to convert the raw JSON value into a proper + * Channel. + * @param jsonPaths Whole JSON path, where the JsonElement for the given + * channel is located. + * @param return type + * @return Value of the last JsonElement by running through the specified JSON + * path. + */ + private static T getValueFromJson(OpenemsType openemsType, JsonElement json, Function converter, + String... jsonPaths) { + + var currentJsonElement = json; + // Go through the whole jsonPath of the current channelId + for (var i = 0; i < jsonPaths.length; i++) { + var currentPathMember = jsonPaths[i]; + try { + if (i == jsonPaths.length - 1) { + // Last path element + var value = getJsonElementValue(currentJsonElement, openemsType, jsonPaths[i]); + + // Return the converted value + return converter.apply(value); + } + // Not last path element + currentJsonElement = JsonUtils.getAsJsonObject(currentJsonElement, currentPathMember); + } catch (OpenemsNamedException e) { + return null; + } + } + return null; + } + + /** + * Handles a Response froma http call for the endpoint /api GET. + * + * @param response the {@link HttpResponse} to be handled + * @param phaseRotation the configured {@link PhaseRotation} + * @throws OpenemsNamedException when json can not be parsed + */ + public void handleGetApiCallResponse(HttpResponse response, PhaseRotation phaseRotation) + throws OpenemsNamedException { + final var json = parseToJsonObject(response.data()); + for (var channelId : EvcsHardyBarth.ChannelId.values()) { + var jsonPaths = channelId.getJsonPaths(); + var value = getValueFromJson(channelId.doc().getType(), json, channelId.converter, jsonPaths); + + // Set the channel-value + this.parent.channel(channelId).setNextValue(value); + + if (channelId.equals(EvcsHardyBarth.ChannelId.RAW_SALIA_PUBLISH)) { + this.parent.masterEvcs = false; + } + + } + this.setEvcsChannelIds(json, phaseRotation); + } + + /** + * Get Value of the given JsonElement in the required type. + * + * @param jsonElement Element as JSON. + * @param openemsType Required type. + * @param memberName Member name of the JSON Element. + * @return Value in the required type. + * @throws OpenemsNamedException Failed to get the value. + */ + private static Object getJsonElementValue(JsonElement jsonElement, OpenemsType openemsType, String memberName) + throws OpenemsNamedException { + return switch (openemsType) { + case BOOLEAN -> JsonUtils.getAsInt(jsonElement, memberName) == 1; + case DOUBLE -> JsonUtils.getAsDouble(jsonElement, memberName); + case FLOAT -> JsonUtils.getAsFloat(jsonElement, memberName); + case INTEGER -> JsonUtils.getAsInt(jsonElement, memberName); + case LONG -> JsonUtils.getAsLong(jsonElement, memberName); + case SHORT -> JsonUtils.getAsShort(jsonElement, memberName); + case STRING -> JsonUtils.getAsString(jsonElement, memberName); + }; + } +} \ No newline at end of file diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadWorker.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadWorker.java deleted file mode 100644 index c24f974ba21..00000000000 --- a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadWorker.java +++ /dev/null @@ -1,318 +0,0 @@ -package io.openems.edge.evcs.hardybarth; - -import java.util.function.Function; - -import com.google.gson.JsonElement; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.types.OpenemsType; -import io.openems.common.utils.JsonUtils; -import io.openems.common.worker.AbstractCycleWorker; -import io.openems.edge.common.channel.ChannelId; -import io.openems.edge.common.type.TypeUtils; -import io.openems.edge.evcs.api.Evcs; -import io.openems.edge.evcs.api.Status; - -public class HardyBarthReadWorker extends AbstractCycleWorker { - - private final EvcsHardyBarthImpl parent; - private int chargingFinishedCounter = 0; - private int errorCounter = 0; - - public HardyBarthReadWorker(EvcsHardyBarthImpl parent) { - this.parent = parent; - } - - @Override - protected void forever() throws OpenemsNamedException { - - // TODO: Read separate JSON files - // - separate configuration -> this.api.sendGetRequest("/saliaconf.json"); - // e.g. min & max hardware power - // - customeredit values -> this.api.sendGetRequest("/customer.json"); - // - rfidtags -> this.api.sendGetRequest("/rfidtags.json"); - // - chargelogs -> this.api.sendGetRequest("/chargelogs.json"); - // - Read separate saliaconf.json and set minimum and maximum dynamically - - var json = this.parent.api.sendGetRequest("/api"); - if (json == null) { - return; - } - - // Set value for every HardyBarth.ChannelId - for (EvcsHardyBarth.ChannelId channelId : EvcsHardyBarth.ChannelId.values()) { - var jsonPaths = channelId.getJsonPaths(); - var value = this.getValueFromJson(channelId, json, channelId.converter, jsonPaths); - - // Set the channel-value - this.parent.channel(channelId).setNextValue(value); - - if (channelId.equals(EvcsHardyBarth.ChannelId.RAW_SALIA_PUBLISH)) { - this.parent.masterEvcs = false; - } - } - - // Set value for every Evcs.ChannelId - this.setEvcsChannelIds(json); - } - - /** - * Set the value for every Evcs.ChannelId. - * - * @param json Given raw data in JSON - */ - private void setEvcsChannelIds(JsonElement json) { - - // ENERGY_SESSION - var energy = (Double) this.getValueFromJson(Evcs.ChannelId.ENERGY_SESSION, OpenemsType.STRING, json, value -> { - if (value == null) { - return null; - } - Double rawEnergy = null; - String[] chargedata = value.toString().split("\\|"); - if (chargedata.length == 3) { - Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, chargedata[2]); - rawEnergy = doubleValue * 1000; - } - return rawEnergy; - - }, "secc", "port0", "salia", "chargedata"); - this.parent._setEnergySession(energy == null ? null : (int) Math.round(energy)); - - // ACTIVE_CONSUMPTION_ENERGY - var activeConsumptionEnergy = (Long) this.getValueFromJson(Evcs.ChannelId.ACTIVE_CONSUMPTION_ENERGY, json, - value -> { - Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, value); - return TypeUtils.getAsType(OpenemsType.LONG, doubleValue); - - }, "secc", "port0", "metering", "energy", "active_import", "actual"); // - this.parent._setActiveConsumptionEnergy(activeConsumptionEnergy); - - // PHASES - var powerL1 = (Long) this.getValueForChannel(EvcsHardyBarth.ChannelId.RAW_ACTIVE_POWER_L1, json); - var powerL2 = (Long) this.getValueForChannel(EvcsHardyBarth.ChannelId.RAW_ACTIVE_POWER_L2, json); - var powerL3 = (Long) this.getValueForChannel(EvcsHardyBarth.ChannelId.RAW_ACTIVE_POWER_L3, json); - - // TODO: Handle phases, having each phase value in the Nature - // Keep last value if no power value was given - var phases = this.parent.getPhasesAsInt(); - if (powerL1 != null && powerL2 != null && powerL3 != null) { - - var sum = powerL1 + powerL2 + powerL3; - - if (sum > 300) { - phases = 0; - - if (powerL1 >= 100) { - phases += 1; - } - if (powerL2 >= 100) { - phases += 1; - } - if (powerL3 >= 100) { - phases += 1; - } - } - } - this.parent._setPhases(phases); - this.parent.debugLog("Used phases: " + phases); - - // CHARGE_POWER - var chargePowerLong = (Long) this.getValueFromJson(Evcs.ChannelId.CHARGE_POWER, json, value -> { - Integer integerValue = TypeUtils.getAsType(OpenemsType.INTEGER, value); - if (integerValue == null) { - return null; - } - - long activePower = Math.round(integerValue * EvcsHardyBarth.SCALE_FACTOR_MINUS_1); - - // Ignore the consumption of the charger itself - return activePower < 100 ? 0 : activePower; - }, "secc", "port0", "metering", "power", "active_total", "actual"); - - this.parent._setChargePower(chargePowerLong == null ? null : chargePowerLong.intValue()); - - // STATUS - var status = (Status) this.getValueFromJson(EvcsHardyBarth.ChannelId.RAW_CHARGE_STATUS_CHARGEPOINT, json, - value -> { - - String stringValue = TypeUtils.getAsType(OpenemsType.STRING, value); - if (stringValue == null) { - this.errorCounter++; - this.parent.debugLog("Hardy Barth RAW_STATUS would be null! Raw value: " + value); - if (this.errorCounter > 3) { - return Status.ERROR; - } - return this.parent.getStatus(); - } - - Status rawStatus = switch (stringValue) { - case "A" -> Status.NOT_READY_FOR_CHARGING; - case "B" -> { - var tmpStatus = Status.READY_FOR_CHARGING; - - // Detect if the car is full - int chargePower = chargePowerLong == null ? 0 : chargePowerLong.intValue(); - if (this.parent.getSetChargePowerLimit().orElse(0) >= this.parent.getMinimumHardwarePower() - .orElse(0) && chargePower <= 0) { - - if (this.chargingFinishedCounter >= 90) { - tmpStatus = Status.CHARGING_FINISHED; - } else { - this.chargingFinishedCounter++; - } - } else { - this.chargingFinishedCounter = 0; - - // Charging rejected because we are forcing to pause charging - if (this.parent.getSetChargePowerLimit().orElse(0) == 0) { - tmpStatus = Status.CHARGING_REJECTED; - } - } - yield tmpStatus; - } - case "C", "D" -> Status.CHARGING; - case "E", "F" -> { - this.errorCounter++; - this.parent.debugLog("Hardy Barth RAW_STATUS would be an error! Raw value: " + stringValue - + " - Error counter: " + this.errorCounter); - if (this.errorCounter > 3) { - yield Status.ERROR; - } - yield this.parent.getStatus(); - } - default -> { - this.parent.debugLog("State " + stringValue + " is not a valid state"); - yield Status.UNDEFINED; - } - }; - - if (!stringValue.equals("B")) { - this.chargingFinishedCounter = 0; - } - if (!stringValue.equals("E") || !stringValue.equals("F")) { - this.errorCounter = 0; - } - - return rawStatus; - }, "secc", "port0", "ci", "charge", "cp", "status"); - - this.parent._setStatus(status); - } - - /** - * Call the getValueFromJson with the detailed information of the channel. - * - * @param channelId Channel that value will be detect. - * @param json Whole JSON path, where the JsonElement for the given channel - * is located. - * @return Value of the last JsonElement by running through the specified JSON - * path. - */ - private Object getValueForChannel(EvcsHardyBarth.ChannelId channelId, JsonElement json) { - return this.getValueFromJson(channelId, json, channelId.converter, channelId.getJsonPaths()); - } - - /** - * Call the getValueFromJson without a divergent type in the raw json. - * - * @param channelId Channel that value will be detect. - * @param json Raw JsonElement. - * @param converter Converter, to convert the raw JSON value into a proper - * Channel. - * @param jsonPaths Whole JSON path, where the JsonElement for the given channel - * is located. - * @return Value of the last JsonElement by running through the specified JSON - * path. - */ - private Object getValueFromJson(ChannelId channelId, JsonElement json, Function converter, - String... jsonPaths) { - return this.getValueFromJson(channelId, null, json, converter, jsonPaths); - } - - /** - * Get the last JSON element and it's value, by running through the given - * jsonPath. - * - * @param channelId Channel that value will be detect. - * @param divergentTypeInRawJson Divergent type of the value in the depending - * JsonElement. - * @param json Raw JsonElement. - * @param converter Converter, to convert the raw JSON value into a - * proper Channel. - * @param jsonPaths Whole JSON path, where the JsonElement for the - * given channel is located. - * @return Value of the last JsonElement by running through the specified JSON - * path. - */ - private Object getValueFromJson(ChannelId channelId, OpenemsType divergentTypeInRawJson, JsonElement json, - Function converter, String... jsonPaths) { - - var currentJsonElement = json; - // Go through the whole jsonPath of the current channelId - for (var i = 0; i < jsonPaths.length; i++) { - var currentPathMember = jsonPaths[i]; - // System.out.println(currentPathMember); - try { - if (i == jsonPaths.length - 1) { - // - var openemsType = divergentTypeInRawJson == null ? channelId.doc().getType() - : divergentTypeInRawJson; - - // Last path element - var value = this.getJsonElementValue(currentJsonElement, openemsType, jsonPaths[i]); - - // Return the converted value - return converter.apply(value); - } - // Not last path element - currentJsonElement = JsonUtils.getAsJsonObject(currentJsonElement, currentPathMember); - } catch (OpenemsNamedException e) { - return null; - } - } - return null; - } - - /** - * Get Value of the given JsonElement in the required type. - * - * @param jsonElement Element as JSON. - * @param openemsType Required type. - * @param memberName Member name of the JSON Element. - * @return Value in the required type. - * @throws OpenemsNamedException Failed to get the value. - */ - private Object getJsonElementValue(JsonElement jsonElement, OpenemsType openemsType, String memberName) - throws OpenemsNamedException { - final Object value; - - switch (openemsType) { - case BOOLEAN: - value = JsonUtils.getAsInt(jsonElement, memberName) == 1; - break; - case DOUBLE: - value = JsonUtils.getAsDouble(jsonElement, memberName); - break; - case FLOAT: - value = JsonUtils.getAsFloat(jsonElement, memberName); - break; - case INTEGER: - value = JsonUtils.getAsInt(jsonElement, memberName); - break; - case LONG: - value = JsonUtils.getAsLong(jsonElement, memberName); - break; - case SHORT: - value = JsonUtils.getAsShort(jsonElement, memberName); - break; - case STRING: - value = JsonUtils.getAsString(jsonElement, memberName); - break; - default: - value = JsonUtils.getAsString(jsonElement, memberName); - break; - } - return value; - } -} diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthWriteHandler.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthWriteHandler.java new file mode 100644 index 00000000000..cde25123e84 --- /dev/null +++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthWriteHandler.java @@ -0,0 +1,31 @@ +package io.openems.edge.evcs.hardybarth; + +import java.util.concurrent.CompletableFuture; + +import io.openems.edge.evcs.api.ManagedEvcs; +import io.openems.edge.evcs.api.WriteHandler; + +public class HardyBarthWriteHandler extends WriteHandler { + + private CompletableFuture applyChargePowerTask = CompletableFuture.completedFuture(null); + + public HardyBarthWriteHandler(ManagedEvcs parent) { + super(parent); + } + + @Override + protected synchronized void applyChargePower(int power) { + if (!this.applyChargePowerTask.isDone()) { + return; + } + this.applyChargePowerTask = CompletableFuture.runAsync(() -> { + super.applyChargePower(power); + }); + } + + @Override + public synchronized void cancelChargePower() { + this.applyChargePowerTask.cancel(true); + } + +} \ No newline at end of file diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/translation_de.properties b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/translation_de.properties new file mode 100644 index 00000000000..8c5792bf010 --- /dev/null +++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/translation_de.properties @@ -0,0 +1 @@ +noMeterAvailable = Keine Zählerwerte verfügbar. Das Kommunikationskabel des (internen) Zählers ist möglicherweise lose. \ No newline at end of file diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/translation_en.properties b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/translation_en.properties new file mode 100644 index 00000000000..4263f535f21 --- /dev/null +++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/translation_en.properties @@ -0,0 +1 @@ +noMeterAvailable = No meter values available. The communication cable of the (internal) meter may be loose. \ No newline at end of file diff --git a/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java b/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java index 2d44360154f..8ecb6c334bc 100644 --- a/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java +++ b/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java @@ -1,23 +1,278 @@ package io.openems.edge.evcs.hardybarth; +import static io.openems.common.types.HttpStatus.OK; +import static io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory.ofDummyBridge; +import static io.openems.edge.evcs.api.PhaseRotation.L2_L3_L1; +import static io.openems.edge.evcs.api.Phases.THREE_PHASE; +import static io.openems.edge.evcs.api.Status.CHARGING; + import org.junit.Test; +import io.openems.edge.bridge.http.api.HttpResponse; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.evcs.api.DeprecatedEvcs; +import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.meter.api.ElectricityMeter; public class EvcsHardyBarthImplTest { - private static final String COMPONENT_ID = "evcs0"; - @Test public void test() throws Exception { - new ComponentTest(new EvcsHardyBarthImpl()) // + final var phaseRotation = L2_L3_L1; + var sut = new EvcsHardyBarthImpl(); + var ru = sut.readUtils; + new ComponentTest(sut) // + .addReference("httpBridgeFactory", ofDummyBridge()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("evcs0") // .setIp("192.168.8.101") // .setMaxHwCurrent(32_000) // .setMinHwCurrent(6_000) // - .build()) - .next(new TestCase()); + .setPhaseRotation(phaseRotation).build()) + + .next(new TestCase() // + .onBeforeProcessImage(() -> ru + .handleGetApiCallResponse(new HttpResponse(OK, API_RESPONSE), phaseRotation)) // + .output(EvcsHardyBarth.ChannelId.RAW_EVSE_GRID_CURRENT_LIMIT, 16) // + .output(EvcsHardyBarth.ChannelId.RAW_PHASE_COUNT, 3) // + .output(EvcsHardyBarth.ChannelId.RAW_CHARGE_STATUS_PLUG, "locked") // + .output(EvcsHardyBarth.ChannelId.RAW_CHARGE_STATUS_CONTACTOR, "closed") // + .output(EvcsHardyBarth.ChannelId.RAW_CHARGE_STATUS_PWM, "10.00") // + .output(EvcsHardyBarth.ChannelId.RAW_CHARGE_STATUS_CHARGEPOINT, "C") // + .output(EvcsHardyBarth.ChannelId.RAW_SALIA_CHARGE_MODE, "manual") // + .output(EvcsHardyBarth.ChannelId.RAW_SALIA_CHANGE_METER, null) // + .output(EvcsHardyBarth.ChannelId.RAW_SALIA_AUTHMODE, "free") // + .output(EvcsHardyBarth.ChannelId.RAW_SALIA_FIRMWARESTATE, "idle") // + .output(EvcsHardyBarth.ChannelId.RAW_SALIA_FIRMWAREPROGRESS, "0") // + .output(EvcsHardyBarth.ChannelId.RAW_SALIA_PUBLISH, null) // + .output(EvcsHardyBarth.ChannelId.RAW_SESSION_STATUS_AUTHORIZATION, "") // + .output(EvcsHardyBarth.ChannelId.RAW_SESSION_SLAC_STARTED, null) // + .output(EvcsHardyBarth.ChannelId.RAW_SESSION_AUTHORIZATION_METHOD, null) // + .output(EvcsHardyBarth.ChannelId.RAW_CONTACTOR_HLC_TARGET, "0") // + .output(EvcsHardyBarth.ChannelId.RAW_CONTACTOR_ACTUAL, "1") // + .output(EvcsHardyBarth.ChannelId.RAW_CONTACTOR_TARGET, "1") // + .output(EvcsHardyBarth.ChannelId.RAW_CONTACTOR_ERROR, "0") // + .output(EvcsHardyBarth.ChannelId.RAW_METER_SERIALNUMBER, "21031835") // + .output(EvcsHardyBarth.ChannelId.RAW_METER_TYPE, "klefr") // + .output(EvcsHardyBarth.ChannelId.RAW_METER_AVAILABLE, true) // + .output(EvcsHardyBarth.ChannelId.METER_NOT_AVAILABLE, false) // + .output(EvcsHardyBarth.ChannelId.RAW_ACTIVE_ENERGY_TOTAL, 4658050.0) // + .output(EvcsHardyBarth.ChannelId.RAW_ACTIVE_ENERGY_EXPORT, 0.0) // + .output(EvcsHardyBarth.ChannelId.RAW_EMERGENCY_SHUTDOWN, "0") // + .output(EvcsHardyBarth.ChannelId.RAW_RCD_AVAILABLE, false) // + .output(EvcsHardyBarth.ChannelId.RAW_PLUG_LOCK_STATE_ACTUAL, "1") // + .output(EvcsHardyBarth.ChannelId.RAW_PLUG_LOCK_STATE_TARGET, "1") // + .output(EvcsHardyBarth.ChannelId.RAW_PLUG_LOCK_ERROR, "0") // + .output(EvcsHardyBarth.ChannelId.RAW_CP_STATE, "C") // + .output(EvcsHardyBarth.ChannelId.RAW_DIODE_PRESENT, "1") // + .output(EvcsHardyBarth.ChannelId.RAW_CABLE_CURRENT_LIMIT, "-1") // + .output(EvcsHardyBarth.ChannelId.RAW_VENTILATION_STATE_ACTUAL, "0") // + .output(EvcsHardyBarth.ChannelId.RAW_VENTILATION_STATE_TARGET, null) // + .output(EvcsHardyBarth.ChannelId.RAW_VENTILATION_AVAILABLE, false) // + .output(EvcsHardyBarth.ChannelId.RAW_EV_PRESENT, "1") // + .output(EvcsHardyBarth.ChannelId.RAW_CHARGING, "1") // + .output(EvcsHardyBarth.ChannelId.RAW_RFID_AUTHORIZEREQ, "") // + .output(EvcsHardyBarth.ChannelId.RAW_RFID_AVAILABLE, false) // + .output(EvcsHardyBarth.ChannelId.RAW_GRID_CURRENT_LIMIT, "6") // + .output(EvcsHardyBarth.ChannelId.RAW_SLAC_ERROR, null) // + .output(EvcsHardyBarth.ChannelId.RAW_DEVICE_PRODUCT, "2310007") // + .output(EvcsHardyBarth.ChannelId.RAW_DEVICE_MODELNAME, "Salia PLCC Slave") // + .output(EvcsHardyBarth.ChannelId.RAW_DEVICE_HARDWARE_VERSION, "1.0") // + .output(EvcsHardyBarth.ChannelId.RAW_DEVICE_SOFTWARE_VERSION, "1.50.0") // + .output(EvcsHardyBarth.ChannelId.RAW_DEVICE_VCS_VERSION, "V0R5e") // + .output(EvcsHardyBarth.ChannelId.RAW_DEVICE_HOSTNAME, "salia") // + .output(EvcsHardyBarth.ChannelId.RAW_DEVICE_MAC_ADDRESS, "00:01:87:13:12:34") // + .output(EvcsHardyBarth.ChannelId.RAW_DEVICE_SERIAL, 101249323L) // + .output(EvcsHardyBarth.ChannelId.RAW_DEVICE_UUID, "5491ad62-022a-4356-a32c-00018713102x") // + + .output(Evcs.ChannelId.ENERGY_SESSION, 3460) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, 4658050L) // + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, 4658050L) // + .output(Evcs.ChannelId.PHASES, THREE_PHASE) // + .output(Evcs.ChannelId.STATUS, CHARGING) // + .output(DeprecatedEvcs.ChannelId.CHARGE_POWER, 3192) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 3192) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 1044) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, 1075) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, 1073) // + .output(ElectricityMeter.ChannelId.CURRENT, 14_770) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 4_770) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 5_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, 5_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE, 216_156) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 218_868) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 215_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 214_600) // + ); } + + private static final String API_RESPONSE = """ + { + "device":{ + "product":"2310007", + "modelname":"Salia PLCC Slave", + "hardware_version":"1.0", + "software_version":"1.50.0", + "vcs_version":"V0R5e", + "hostname":"salia", + "mac_address":"00:01:87:13:12:34", + "serial":"101249323", + "uuid":"5491ad62-022a-4356-a32c-00018713102x", + "internal_id":"412009" + }, + "secc":{ + "port0":{ + "ci":{ + "evse":{ + "basic":{ + "grid_current_limit":{ + "actual":"16" + }, + "phase_count":"3", + "physical_current_limit":"16", + "offered_current_limit":"6.0" + }, + "phase":{ + "actual":"3" + } + }, + "charge":{ + "cp":{ + "status":"C" + }, + "plug":{ + "status":"locked" + }, + "contactor":{ + "status":"closed" + }, + "pwm":{ + "status":"10.00" + } + } + }, + "salia":{ + "chargemode":"manual", + "thermal":"52893", + "mem":"392276", + "uptime":" 1:04", + "load":"0.37", + "chargedata":"3813|3192|3.46|", + "authmode":"free", + "firmwarestate":"idle", + "firmwareprogress":"0", + "heartbeat":"off", + "pausecharging":"0" + }, + "session":{ + "authorization_status":"" + }, + "contactor":{ + "state":{ + "hlc_target":"0", + "actual":"1", + "target":"1" + }, + "error":"0" + }, + "metering":{ + "meter":{ + "serialnumber":"21031835", + "type":"klefr", + "available":"1" + }, + "eichrecht_protocol":"none", + "power":{ + "active":{ + "ac":{ + "l1":{ + "actual":"10750" + }, + "l2":{ + "actual":"10730" + }, + "l3":{ + "actual":"10440" + } + } + }, + "active_total":{ + "actual":"31920" + } + }, + "current":{ + "ac":{ + "l1":{ + "actual":"5000" + }, + "l2":{ + "actual":"5000" + }, + "l3":{ + "actual":"4770" + } + } + }, + "energy":{ + "active_total":{ + "actual":"4658050" + }, + "active_export":{ + "actual":"0" + }, + "active_import":{ + "actual":"4658050" + } + } + }, + "emergency_shutdown":"0", + "rcd":{ + "feedback":{ + "available":"1" + }, + "state":{ + "actual":"1" + }, + "recloser":{ + "available":"0" + } + }, + "plug_lock":{ + "state":{ + "actual":"1", + "target":"1" + }, + "error":"0" + }, + "availability":{ + "actual":"operative" + }, + "cp":{ + "pwm_state":{ + "actual":"1" + }, + "state":"C", + "duty_cycle":"10.00" + }, + "rfid":{ + "available":"0", + "authorizereq":"" + }, + "diode_present":"1", + "cable_current_limit":"-1", + "ready_for_slac":"0", + "ev_present":"1", + "ventilation":{ + "state":{ + "actual":"0" + }, + "available":"0" + }, + "charging":"1", + "grid_current_limit":"6" + } + } + } + """; } diff --git a/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/MyConfig.java b/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/MyConfig.java index 98cc8717cff..d3a6ee1c74a 100644 --- a/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/MyConfig.java +++ b/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/MyConfig.java @@ -1,6 +1,7 @@ package io.openems.edge.evcs.hardybarth; import io.openems.common.test.AbstractComponentConfig; +import io.openems.edge.evcs.api.PhaseRotation; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { @@ -10,6 +11,7 @@ protected static class Builder { private String ip; private int minHwCurrent; private int maxHwCurrent; + private PhaseRotation phaseRotation; private Builder() { } @@ -34,6 +36,11 @@ public Builder setMaxHwCurrent(int maxHwCurrent) { return this; } + public Builder setPhaseRotation(PhaseRotation phaseRotation) { + this.phaseRotation = phaseRotation; + return this; + } + public MyConfig build() { return new MyConfig(this); } @@ -74,4 +81,9 @@ public int minHwCurrent() { public int maxHwCurrent() { return this.builder.maxHwCurrent; } + + @Override + public PhaseRotation phaseRotation() { + return this.builder.phaseRotation; + } } \ No newline at end of file diff --git a/io.openems.edge.evcs.keba.kecontact/bnd.bnd b/io.openems.edge.evcs.keba.kecontact/bnd.bnd index c1057f1716f..507c805f4d6 100644 --- a/io.openems.edge.evcs.keba.kecontact/bnd.bnd +++ b/io.openems.edge.evcs.keba.kecontact/bnd.bnd @@ -7,7 +7,8 @@ Bundle-Version: 1.0.0.${tstamp} ${buildpath},\ io.openems.common,\ io.openems.edge.common,\ - io.openems.edge.evcs.api + io.openems.edge.evcs.api,\ + io.openems.edge.meter.api,\ -testpath: \ ${testpath} diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/Config.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/Config.java index 66e9e587fde..5bed47bdad4 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/Config.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/Config.java @@ -3,6 +3,8 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import io.openems.edge.evcs.api.PhaseRotation; + @ObjectClassDefinition(name = "EVCS KEBA KeContact", // description = "Implements the KEBA KeContact P20/P30 electric vehicle charging station.") @interface Config { @@ -25,6 +27,9 @@ @AttributeDefinition(name = "Minimum power", description = "Minimum current of the Charger in mA.", required = true) int minHwCurrent() default 6000; + @AttributeDefinition(name = "Phase Rotation", description = "Apply standard or rotated wiring") + PhaseRotation phaseRotation() default PhaseRotation.L1_L2_L3; + @AttributeDefinition(name = "Use display?", description = "Activates the KEBA display to show the current power or states.", required = true) boolean useDisplay() default true; diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContact.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContact.java index e392f6cec85..97f8a70c07c 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContact.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContact.java @@ -16,8 +16,10 @@ import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.Status; +import io.openems.edge.meter.api.ElectricityMeter; -public interface EvcsKebaKeContact extends ManagedEvcs, Evcs, OpenemsComponent, EventHandler, ModbusSlave { +public interface EvcsKebaKeContact + extends ManagedEvcs, Evcs, ElectricityMeter, OpenemsComponent, EventHandler, ModbusSlave { public static final int UDP_PORT = 7090; @@ -85,42 +87,16 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { /* * Report 3 */ - VOLTAGE_L1(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.VOLT) // - .text("Voltage on L1")), // - VOLTAGE_L2(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.VOLT) // - .text("Voltage on L2")), // - VOLTAGE_L3(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.VOLT) // - .text("Voltage on L3")), // - CURRENT_L1(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE) // - .text("Current on L1")), // - CURRENT_L2(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE) // - .text("Current on L2")), // - CURRENT_L3(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE) // - .text("Current on L3")), // - ACTUAL_POWER(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIWATT) // - .text("Total real power")), // COS_PHI(Doc.of(OpenemsType.INTEGER) // .unit(Unit.PERCENT) // .text("Power factor")), // - ENERGY_TOTAL(Doc.of(OpenemsType.LONG) // - .unit(Unit.CUMULATED_WATT_HOURS) // - .text("Total power consumption (persistent) without current loading session. " - + "Is summed up after each completed charging session")), // DIP_SWITCH_ERROR_1_3_NOT_SET_FOR_COMM(Doc.of(Level.FAULT) // .debounce(5, Debounce.TRUE_VALUES_IN_A_ROW_TO_SET_TRUE) // .text("Dip-Switch 1.3. for communication must be on")), // DIP_SWITCH_ERROR_2_6_NOT_SET_FOR_STATIC_IP(Doc.of(Level.FAULT) // .debounce(5, Debounce.TRUE_VALUES_IN_A_ROW_TO_SET_TRUE) // .text("A static ip is configured. The Dip-Switch 2.6. must be on")), // - DIP_SWITCH_ERROR_2_6_SET_FOR_DYNAMIC_IP(Doc.of(Level.FAULT) // - .debounce(5, Debounce.TRUE_VALUES_IN_A_ROW_TO_SET_TRUE) // + DIP_SWITCH_ERROR_2_6_SET_FOR_DYNAMIC_IP(Doc.of(OpenemsType.BOOLEAN) // .text("A dynamic ip is configured. Either the Dip-Switch 2.6. must be off or a static ip has to be configured")), // DIP_SWITCH_INFO_2_5_SET_FOR_MASTER_SLAVE_COMM(Doc.of(Level.INFO) // .debounce(5, Debounce.TRUE_VALUES_IN_A_ROW_TO_SET_TRUE) // @@ -180,19 +156,10 @@ private ModbusSlaveNatureTable getModbusSlaveNatureTable(AccessMode accessMode) .channel(72, EvcsKebaKeContact.ChannelId.CURR_FAILSAFE, ModbusType.UINT16) .channel(73, EvcsKebaKeContact.ChannelId.TIMEOUT_FAILSAFE, ModbusType.UINT16) .channel(74, EvcsKebaKeContact.ChannelId.CURR_TIMER, ModbusType.UINT16) - .channel(75, EvcsKebaKeContact.ChannelId.TIMEOUT_CT, ModbusType.UINT16).uint16Reserved(76) + .channel(75, EvcsKebaKeContact.ChannelId.TIMEOUT_CT, ModbusType.UINT16) // + .uint16Reserved(76) // .channel(77, EvcsKebaKeContact.ChannelId.OUTPUT, ModbusType.UINT16) .channel(78, EvcsKebaKeContact.ChannelId.INPUT, ModbusType.UINT16) - - // Report 3 - .channel(79, EvcsKebaKeContact.ChannelId.VOLTAGE_L1, ModbusType.UINT16) - .channel(80, EvcsKebaKeContact.ChannelId.VOLTAGE_L2, ModbusType.UINT16) - .channel(81, EvcsKebaKeContact.ChannelId.VOLTAGE_L3, ModbusType.UINT16) - .channel(82, EvcsKebaKeContact.ChannelId.CURRENT_L1, ModbusType.UINT16) - .channel(83, EvcsKebaKeContact.ChannelId.CURRENT_L2, ModbusType.UINT16) - .channel(84, EvcsKebaKeContact.ChannelId.CURRENT_L3, ModbusType.UINT16) - .channel(85, EvcsKebaKeContact.ChannelId.ACTUAL_POWER, ModbusType.UINT16) - .channel(86, EvcsKebaKeContact.ChannelId.COS_PHI, ModbusType.UINT16).uint16Reserved(87) - .channel(88, EvcsKebaKeContact.ChannelId.ENERGY_TOTAL, ModbusType.UINT16).build(); + .build(); } } diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImpl.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImpl.java index c036301b188..3df576d33cc 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImpl.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImpl.java @@ -23,17 +23,20 @@ import org.slf4j.LoggerFactory; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.modbusslave.ModbusSlave; import io.openems.edge.evcs.api.AbstractManagedEvcsComponent; import io.openems.edge.evcs.api.ChargingType; +import io.openems.edge.evcs.api.DeprecatedEvcs; import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.EvcsPower; import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.Phases; import io.openems.edge.evcs.keba.kecontact.core.EvcsKebaKeContactCore; +import io.openems.edge.meter.api.ElectricityMeter; @Designate(ocd = Config.class, factory = true) @Component(// @@ -44,12 +47,13 @@ @EventTopics({ // EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, // }) -public class EvcsKebaKeContactImpl extends AbstractManagedEvcsComponent - implements EvcsKebaKeContact, ManagedEvcs, Evcs, OpenemsComponent, EventHandler, ModbusSlave { +public class EvcsKebaKeContactImpl extends AbstractManagedEvcsComponent implements EvcsKebaKeContact, ManagedEvcs, Evcs, + DeprecatedEvcs, ElectricityMeter, OpenemsComponent, EventHandler, ModbusSlave { + + protected final ReadHandler readHandler = new ReadHandler(this); private final Logger log = LoggerFactory.getLogger(EvcsKebaKeContactImpl.class); private final ReadWorker readWorker = new ReadWorker(this); - private final ReadHandler readHandler = new ReadHandler(this); @Reference private EvcsPower evcsPower; @@ -65,10 +69,21 @@ public class EvcsKebaKeContactImpl extends AbstractManagedEvcsComponent public EvcsKebaKeContactImpl() { super(// OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // ManagedEvcs.ChannelId.values(), // Evcs.ChannelId.values(), // + DeprecatedEvcs.ChannelId.values(), // EvcsKebaKeContact.ChannelId.values() // ); + DeprecatedEvcs.copyToDeprecatedEvcsChannels(this); + ElectricityMeter.calculateSumCurrentFromPhases(this); + ElectricityMeter.calculateAverageVoltageFromPhases(this); + + // Set ReactivePower defaults + this._setReactivePower(0); + this._setReactivePowerL1(0); + this._setReactivePowerL2(0); + this._setReactivePowerL3(0); } @Activate @@ -127,6 +142,11 @@ public void handleEvent(Event event) { } } + @Override + public MeterType getMeterType() { + return MeterType.MANAGED_CONSUMPTION_METERED; + } + /** * Send UDP message to KEBA KeContact. Returns true if sent successfully * diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java index 3fafa14550a..0bcb9a43364 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java @@ -1,19 +1,27 @@ package io.openems.edge.evcs.keba.kecontact; +import static io.openems.common.utils.JsonUtils.getAsOptionalInt; +import static io.openems.common.utils.JsonUtils.getAsOptionalLong; +import static io.openems.common.utils.JsonUtils.getAsOptionalString; +import static io.openems.edge.evcs.api.Evcs.evaluatePhaseCount; +import static io.openems.edge.evcs.api.Phases.THREE_PHASE; +import static io.openems.edge.evcs.api.Status.CHARGING; +import static java.lang.Math.round; + import java.math.BigInteger; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.channel.Channel; +import io.openems.edge.common.channel.ChannelId; import io.openems.edge.evcs.api.Evcs; -import io.openems.edge.evcs.api.Phases; +import io.openems.edge.evcs.api.PhaseRotation.RotatedPhases; import io.openems.edge.evcs.api.Status; /** @@ -34,240 +42,258 @@ public ReadHandler(EvcsKebaKeContactImpl parent) { @Override public void accept(String message) { + final var keba = this.parent; if (message.startsWith("TCH-OK")) { this.log.debug("KEBA confirmed reception of command: TCH-OK"); - this.parent.triggerQuery(); + keba.triggerQuery(); + return; + } - } else if (message.startsWith("TCH-ERR")) { + if (message.startsWith("TCH-ERR")) { this.log.warn("KEBA reported command error: TCH-ERR"); - this.parent.triggerQuery(); + keba.triggerQuery(); + return; + } - } else { - JsonElement jsonMessageElement; - try { - jsonMessageElement = JsonUtils.parse(message); - } catch (OpenemsNamedException e) { - this.log.error("Error while parsing KEBA message: " + e.getMessage()); - return; + keba.logInfoInDebugmode(this.log, message); + + // Parse JsonObject + final JsonObject j; + try { + j = JsonUtils.parseToJsonObject(message); + } catch (OpenemsNamedException e) { + this.log.error("Error while parsing KEBA message: " + e.getMessage()); + return; + } + + switch (getAsOptionalString(j, "ID").orElse("")) { + /* + * report 1 + */ + case "1" -> { + this.receiveReport1 = true; + this.setString(EvcsKebaKeContact.ChannelId.SERIAL, j, "Serial"); + this.setString(EvcsKebaKeContact.ChannelId.FIRMWARE, j, "Firmware"); + this.setInt(EvcsKebaKeContact.ChannelId.COM_MODULE, j, "COM-module"); + + // Dip-Switches + var dipSwitch1 = getAsOptionalString(j, "DIP-Sw1"); + var dipSwitch2 = getAsOptionalString(j, "DIP-Sw2"); + + if (dipSwitch1.isPresent() && dipSwitch2.isPresent()) { + this.checkDipSwitchSettings(dipSwitch1.get(), dipSwitch2.get()); } - this.parent.logInfoInDebugmode(this.log, message); - - var jsonMessage = jsonMessageElement.getAsJsonObject(); - // JsonUtils.prettyPrint(jMessage); - var idOpt = JsonUtils.getAsOptionalString(jsonMessage, "ID"); - if (idOpt.isPresent()) { - // message with ID - var id = idOpt.get(); - if (id.equals("1")) { - /* - * Reply to report 1 - */ - this.receiveReport1 = true; - this.setString(EvcsKebaKeContact.ChannelId.SERIAL, jsonMessage, "Serial"); - this.setString(EvcsKebaKeContact.ChannelId.FIRMWARE, jsonMessage, "Firmware"); - this.setInt(EvcsKebaKeContact.ChannelId.COM_MODULE, jsonMessage, "COM-module"); - - // Dip-Switches - var dipSwitch1 = JsonUtils.getAsOptionalString(jsonMessage, "DIP-Sw1"); - var dipSwitch2 = JsonUtils.getAsOptionalString(jsonMessage, "DIP-Sw2"); - - if (dipSwitch1.isPresent() && dipSwitch2.isPresent()) { - this.checkDipSwitchSettings(dipSwitch1.get(), dipSwitch2.get()); - } - // Product information - var product = JsonUtils.getAsOptionalString(jsonMessage, "Product"); - if (product.isPresent()) { - this.parent.channel(EvcsKebaKeContact.ChannelId.PRODUCT).setNextValue(product.get()); - this.checkProductInformation(product.get()); - } + // Product information + var product = getAsOptionalString(j, "Product"); + keba.channel(EvcsKebaKeContact.ChannelId.PRODUCT).setNextValue(product.orElse(null)); + if (product.isPresent()) { + this.checkProductInformation(product.get()); + } + } - } else if (id.equals("2")) { - /* - * Reply to report 2 - */ - this.receiveReport2 = true; - this.setInt(EvcsKebaKeContact.ChannelId.STATUS_KEBA, jsonMessage, "State"); - - // Value "setenergy" not used, because it is reset by the currtime 0 1 command - - // Set Evcs status - Channel stateChannel = this.parent.channel(EvcsKebaKeContact.ChannelId.STATUS_KEBA); - Channel plugChannel = this.parent.channel(EvcsKebaKeContact.ChannelId.PLUG); - - Plug plug = plugChannel.value().asEnum(); - Status status = stateChannel.value().asEnum(); - if (plug.equals(Plug.PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED)) { - - // Charging is rejected (by the Software) if the plug is connected but the EVCS - // still not ready for charging. - if (status.equals(Status.NOT_READY_FOR_CHARGING)) { - status = Status.CHARGING_REJECTED; - } - - // Charging is Finished if 'Plug' is connected, State was charging or already - // finished and the EVCS is still ready for charging. - var evcsStatus = this.parent.getStatus(); - switch (evcsStatus) { - case CHARGING_REJECTED: - case ENERGY_LIMIT_REACHED: - case ERROR: - case NOT_READY_FOR_CHARGING: - case STARTING: - case UNDEFINED: - break; - case READY_FOR_CHARGING: - case CHARGING: - case CHARGING_FINISHED: - if (status.equals(Status.READY_FOR_CHARGING) - && this.parent.getSetChargePowerLimit().orElse(0) > 0) { - status = Status.CHARGING_FINISHED; - } - break; - } - - /* - * Check if the maximum energy limit is reached, informs the user and sets the - * status - */ - int limit = this.parent.getSetEnergyLimit().orElse(0); - int energy = this.parent.getEnergySession().orElse(0); - if (energy >= limit && limit != 0) { - status = Status.ENERGY_LIMIT_REACHED; - } - } else { - // Plug not fully connected - status = Status.NOT_READY_FOR_CHARGING; - } + /* + * report 2 + */ + case "2" -> { + this.receiveReport2 = true; + this.setInt(EvcsKebaKeContact.ChannelId.STATUS_KEBA, j, "State"); - this.parent._setStatus(status); - var errorState = status == Status.ERROR == true; - this.parent.channel(EvcsKebaKeContact.ChannelId.CHARGINGSTATION_STATE_ERROR) - .setNextValue(errorState); - - this.setInt(EvcsKebaKeContact.ChannelId.ERROR_1, jsonMessage, "Error1"); - this.setInt(EvcsKebaKeContact.ChannelId.ERROR_2, jsonMessage, "Error2"); - this.setInt(EvcsKebaKeContact.ChannelId.PLUG, jsonMessage, "Plug"); - this.setBoolean(EvcsKebaKeContact.ChannelId.ENABLE_SYS, jsonMessage, "Enable sys"); - this.setBoolean(EvcsKebaKeContact.ChannelId.ENABLE_USER, jsonMessage, "Enable user"); - this.setInt(EvcsKebaKeContact.ChannelId.MAX_CURR_PERCENT, jsonMessage, "Max curr %"); - this.setInt(EvcsKebaKeContact.ChannelId.CURR_FAILSAFE, jsonMessage, "Curr FS"); - this.setInt(EvcsKebaKeContact.ChannelId.TIMEOUT_FAILSAFE, jsonMessage, "Tmo FS"); - this.setInt(EvcsKebaKeContact.ChannelId.CURR_TIMER, jsonMessage, "Curr timer"); - this.setInt(EvcsKebaKeContact.ChannelId.TIMEOUT_CT, jsonMessage, "Tmo CT"); - this.setBoolean(EvcsKebaKeContact.ChannelId.OUTPUT, jsonMessage, "Output"); - this.setBoolean(EvcsKebaKeContact.ChannelId.INPUT, jsonMessage, "Input"); - this.setInt(EvcsKebaKeContact.ChannelId.MAX_CURR, jsonMessage, "Curr HW"); - this.setInt(EvcsKebaKeContact.ChannelId.CURR_USER, jsonMessage, "Curr user"); - - } else if (id.equals("3")) { - /* - * Reply to report 3 - */ - this.receiveReport3 = true; - this.setInt(EvcsKebaKeContact.ChannelId.VOLTAGE_L1, jsonMessage, "U1"); - this.setInt(EvcsKebaKeContact.ChannelId.VOLTAGE_L2, jsonMessage, "U2"); - this.setInt(EvcsKebaKeContact.ChannelId.VOLTAGE_L3, jsonMessage, "U3"); - this.setInt(EvcsKebaKeContact.ChannelId.CURRENT_L1, jsonMessage, "I1"); - this.setInt(EvcsKebaKeContact.ChannelId.CURRENT_L2, jsonMessage, "I2"); - this.setInt(EvcsKebaKeContact.ChannelId.CURRENT_L3, jsonMessage, "I3"); - this.setInt(EvcsKebaKeContact.ChannelId.ACTUAL_POWER, jsonMessage, "P"); - this.setInt(EvcsKebaKeContact.ChannelId.COS_PHI, jsonMessage, "PF"); - - long totalEnergy = Math - .round(JsonUtils.getAsOptionalLong(jsonMessage, "E total").orElse(0L) * 0.1F); - this.parent.channel(EvcsKebaKeContact.ChannelId.ENERGY_TOTAL).setNextValue(totalEnergy); - this.parent._setActiveConsumptionEnergy(totalEnergy); - - // Set the count of the Phases that are currently used - Channel currentL1 = this.parent.channel(EvcsKebaKeContact.ChannelId.CURRENT_L1); - Channel currentL2 = this.parent.channel(EvcsKebaKeContact.ChannelId.CURRENT_L2); - Channel currentL3 = this.parent.channel(EvcsKebaKeContact.ChannelId.CURRENT_L3); - var currentSum = currentL1.getNextValue().orElse(0) + currentL2.getNextValue().orElse(0) - + currentL3.getNextValue().orElse(0); - - if (currentSum > 300) { - - this.parent._setStatus(Status.CHARGING); - var phases = 0; - - if (currentL1.getNextValue().orElse(0) >= 100) { - phases += 1; - } - if (currentL2.getNextValue().orElse(0) >= 100) { - phases += 1; - } - if (currentL3.getNextValue().orElse(0) >= 100) { - phases += 1; - } - this.parent._setPhases(phases); - - this.parent.logInfoInDebugmode(this.log, "Used phases: " + phases); - } + // Value "setenergy" not used, because it is reset by the currtime 0 1 command - /* - * Set FIXED_MAXIMUM_HARDWARE_POWER of Evcs - this is setting internally the - * dynamically calculated MAXIMUM_HARDWARE_POWER including the current used - * phases. - */ - Channel maxDipSwitchLimitChannel = this.parent - .channel(EvcsKebaKeContact.ChannelId.DIP_SWITCH_MAX_HW); - int maxDipSwitchPowerLimit = Math.round( - maxDipSwitchLimitChannel.value().orElse(Evcs.DEFAULT_MAXIMUM_HARDWARE_CURRENT) / 1000f) - * Evcs.DEFAULT_VOLTAGE * Phases.THREE_PHASE.getValue(); - - // Minimum of hardware setting and component configuration will be set. - int maximumHardwareLimit = Math.min(maxDipSwitchPowerLimit, - this.parent.getConfiguredMaximumHardwarePower()); - - this.parent._setFixedMaximumHardwarePower(maximumHardwareLimit); - - /* - * Set FIXED_MINIMUM_HARDWARE_POWER of Evcs - this is setting internally the - * dynamically calculated MINIMUM_HARDWARE_POWER including the current used - * phases. - */ - this.parent._setFixedMinimumHardwarePower(this.parent.getConfiguredMinimumHardwarePower()); - - /* - * Set CHARGE_POWER of Evcs - */ - var powerMw = JsonUtils.getAsOptionalInt(jsonMessage, "P"); // in [mW] - Integer power = null; - if (powerMw.isPresent()) { - power = powerMw.get() / 1000; // convert to [W] - } - this.parent.channel(Evcs.ChannelId.CHARGE_POWER).setNextValue(power); + // Set Evcs status + Channel stateChannel = keba.channel(EvcsKebaKeContact.ChannelId.STATUS_KEBA); + Channel plugChannel = keba.channel(EvcsKebaKeContact.ChannelId.PLUG); + + Plug plug = plugChannel.value().asEnum(); + Status status = stateChannel.value().asEnum(); + if (plug.equals(Plug.PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED)) { - /* - * Set ENERGY_SESSION of Evcs - */ - this.parent.channel(Evcs.ChannelId.ENERGY_SESSION) - .setNextValue(JsonUtils.getAsOptionalInt(jsonMessage, "E pres").orElse(0) * 0.1); + // Charging is rejected (by the Software) if the plug is connected but the EVCS + // still not ready for charging. + if (status.equals(Status.NOT_READY_FOR_CHARGING)) { + status = Status.CHARGING_REJECTED; + } + + // Charging is Finished if 'Plug' is connected, State was charging or already + // finished and the EVCS is still ready for charging. + switch (keba.getStatus()) { + case CHARGING_REJECTED: + case ENERGY_LIMIT_REACHED: + case ERROR: + case NOT_READY_FOR_CHARGING: + case STARTING: + case UNDEFINED: + break; + case READY_FOR_CHARGING: + case CHARGING: + case CHARGING_FINISHED: + if (status.equals(Status.READY_FOR_CHARGING) && keba.getSetChargePowerLimit().orElse(0) > 0) { + status = Status.CHARGING_FINISHED; + } + break; } - } else { /* - * message without ID -> UDP broadcast + * Check if the maximum energy limit is reached, informs the user and sets the + * status */ - if (jsonMessage.has("State")) { - this.setInt(EvcsKebaKeContact.ChannelId.STATUS_KEBA, jsonMessage, "State"); - } - if (jsonMessage.has("Plug")) { - this.setInt(EvcsKebaKeContact.ChannelId.PLUG, jsonMessage, "Plug"); - } - if (jsonMessage.has("Input")) { - this.setBoolean(EvcsKebaKeContact.ChannelId.INPUT, jsonMessage, "Input"); - } - if (jsonMessage.has("Enable sys")) { - this.setBoolean(EvcsKebaKeContact.ChannelId.ENABLE_SYS, jsonMessage, "Enable sys"); - } - if (jsonMessage.has("E pres")) { - this.parent.channel(Evcs.ChannelId.ENERGY_SESSION) - .setNextValue(JsonUtils.getAsOptionalInt(jsonMessage, "E pres").orElse(0) * 0.1); + int limit = keba.getSetEnergyLimit().orElse(0); + int energy = keba.getEnergySession().orElse(0); + if (energy >= limit && limit != 0) { + status = Status.ENERGY_LIMIT_REACHED; } + } else { + // Plug not fully connected + status = Status.NOT_READY_FOR_CHARGING; } + + keba._setStatus(status); + var errorState = status == Status.ERROR == true; + keba.channel(EvcsKebaKeContact.ChannelId.CHARGINGSTATION_STATE_ERROR).setNextValue(errorState); + + this.setInt(EvcsKebaKeContact.ChannelId.ERROR_1, j, "Error1"); + this.setInt(EvcsKebaKeContact.ChannelId.ERROR_2, j, "Error2"); + this.setInt(EvcsKebaKeContact.ChannelId.PLUG, j, "Plug"); + this.setBoolean(EvcsKebaKeContact.ChannelId.ENABLE_SYS, j, "Enable sys"); + this.setBoolean(EvcsKebaKeContact.ChannelId.ENABLE_USER, j, "Enable user"); + this.setInt(EvcsKebaKeContact.ChannelId.MAX_CURR_PERCENT, j, "Max curr %"); + this.setInt(EvcsKebaKeContact.ChannelId.CURR_FAILSAFE, j, "Curr FS"); + this.setInt(EvcsKebaKeContact.ChannelId.TIMEOUT_FAILSAFE, j, "Tmo FS"); + this.setInt(EvcsKebaKeContact.ChannelId.CURR_TIMER, j, "Curr timer"); + this.setInt(EvcsKebaKeContact.ChannelId.TIMEOUT_CT, j, "Tmo CT"); + this.setBoolean(EvcsKebaKeContact.ChannelId.OUTPUT, j, "Output"); + this.setBoolean(EvcsKebaKeContact.ChannelId.INPUT, j, "Input"); + this.setInt(EvcsKebaKeContact.ChannelId.MAX_CURR, j, "Curr HW"); + this.setInt(EvcsKebaKeContact.ChannelId.CURR_USER, j, "Curr user"); + } + + /* + * report 3 + */ + case "3" -> { + /* + * Reply to report 3 + */ + this.receiveReport3 = true; + + // Voltage + final var voltageL1 = getAsOptionalInt(j, "U1").map(v -> v != 0 ? v * 1000 : null).orElse(null); + final var voltageL2 = getAsOptionalInt(j, "U2").map(v -> v != 0 ? v * 1000 : null).orElse(null); + final var voltageL3 = getAsOptionalInt(j, "U3").map(v -> v != 0 ? v * 1000 : null).orElse(null); + + // Current + final var currentL1 = getAsOptionalInt(j, "I1").orElse(0).intValue(); + final var currentL2 = getAsOptionalInt(j, "I2").orElse(0).intValue(); + final var currentL3 = getAsOptionalInt(j, "I3").orElse(0).intValue(); + + // Power + final var activePower = getAsOptionalInt(j, "P") // + .map(p -> p / 1000) // convert [mW] to [W] + .orElse(null); + keba._setActivePower(activePower); + + // Round power per phase and apply rotated phases + var appp = ActivePowerPerPhase.from(activePower, // + voltageL1, currentL1, voltageL2, currentL2, voltageL3, currentL3); + var rp = RotatedPhases.from(keba.config.phaseRotation(), // + voltageL1, currentL1, appp.activePowerL1, // + voltageL2, currentL2, appp.activePowerL2, // + voltageL3, currentL3, appp.activePowerL3); + keba._setVoltageL1(rp.voltageL1()); + keba._setVoltageL2(rp.voltageL2()); + keba._setVoltageL3(rp.voltageL3()); + keba._setCurrentL1(rp.currentL1()); + keba._setCurrentL2(rp.currentL2()); + keba._setCurrentL3(rp.currentL3()); + keba._setActivePowerL1(rp.activePowerL1()); + keba._setActivePowerL2(rp.activePowerL2()); + keba._setActivePowerL3(rp.activePowerL3()); + + // Energy + keba._setActiveProductionEnergy(// + getAsOptionalLong(j, "E total") // + .map(e -> round(e * 0.1F)) // + .orElse(null)); + keba._setEnergySession(// + getAsOptionalInt(j, "E pres") // + .map(e -> round(e * 0.1F)) // + .orElse(null)); + + // TODO use COS_PHI to calculate ReactivePower + this.setInt(EvcsKebaKeContact.ChannelId.COS_PHI, j, "PF"); + + final var phases = evaluatePhaseCount(appp.activePowerL1, appp.activePowerL2, appp.activePowerL3); + keba._setPhases(phases); + if (phases != null) { + keba.logInfoInDebugmode(this.log, "Used phases: " + phases); + keba._setStatus(CHARGING); + } + + /* + * Set FIXED_MAXIMUM_HARDWARE_POWER of Evcs - this is setting internally the + * dynamically calculated MAXIMUM_HARDWARE_POWER including the current used + * phases. + */ + Channel maxDipSwitchLimitChannel = keba.channel(EvcsKebaKeContact.ChannelId.DIP_SWITCH_MAX_HW); + int maxDipSwitchPowerLimit = round(maxDipSwitchLimitChannel.value() // + .orElse(Evcs.DEFAULT_MAXIMUM_HARDWARE_CURRENT) / 1000f) * Evcs.DEFAULT_VOLTAGE + * THREE_PHASE.getValue(); + + // Minimum of hardware setting and component configuration will be set. + int maximumHardwareLimit = Math.min(maxDipSwitchPowerLimit, keba.getConfiguredMaximumHardwarePower()); + + keba._setFixedMaximumHardwarePower(maximumHardwareLimit); + + /* + * Set FIXED_MINIMUM_HARDWARE_POWER of Evcs - this is setting internally the + * dynamically calculated MINIMUM_HARDWARE_POWER including the current used + * phases. + */ + keba._setFixedMinimumHardwarePower(keba.getConfiguredMinimumHardwarePower()); + } + + /* + * message without ID -> UDP broadcast + */ + default -> { + if (j.has("State")) { + this.setInt(EvcsKebaKeContact.ChannelId.STATUS_KEBA, j, "State"); + } + if (j.has("Plug")) { + this.setInt(EvcsKebaKeContact.ChannelId.PLUG, j, "Plug"); + } + if (j.has("Input")) { + this.setBoolean(EvcsKebaKeContact.ChannelId.INPUT, j, "Input"); + } + if (j.has("Enable sys")) { + this.setBoolean(EvcsKebaKeContact.ChannelId.ENABLE_SYS, j, "Enable sys"); + } + if (j.has("E pres")) { + keba._setEnergySession(// + getAsOptionalInt(j, "E pres") // + .map(e -> round(e * 0.1F)) // + .orElse(null)); + } + } + } + } + + public record ActivePowerPerPhase(Integer activePowerL1, Integer activePowerL2, Integer activePowerL3) { + protected static ActivePowerPerPhase from(Integer activePowerSum, Integer voltageL1, int currentL1, + Integer voltageL2, int currentL2, Integer voltageL3, int currentL3) { + if (activePowerSum == null) { + return new ActivePowerPerPhase(null, null, null); + } + + var pL1 = voltageL1 != null ? voltageL1 / 1000 * currentL1 : 0; + var pL2 = voltageL2 != null ? voltageL2 / 1000 * currentL2 : 0; + var pL3 = voltageL3 != null ? voltageL3 / 1000 * currentL3 : 0; + var pSum = pL1 + pL2 + pL3; + var factor = activePowerSum / (float) pSum; // distribute power to match sum + + return new ActivePowerPerPhase(round(pL1 * factor), round(pL2 * factor), round(pL3 * factor)); } } @@ -318,29 +344,15 @@ private void checkDipSwitchSettings(String dipSwitch1, String dipSwitch2) { this.setnextStateChannelValue(EvcsKebaKeContact.ChannelId.DIP_SWITCH_INFO_2_8_SET_FOR_INSTALLATION, setState); // Set Channel for the configured maximum limit in mA - Integer hwLimit = null; - var hwLimitDips = dipSwitch1.substring(5); - - switch (hwLimitDips) { - case "000": - hwLimit = 10_000; - break; - case "100": - hwLimit = 13_000; - break; - case "010": - hwLimit = 16_000; - break; - case "110": - hwLimit = 20_000; - break; - case "001": - hwLimit = 25_000; - break; - case "101": - hwLimit = 32_000; - break; - } + var hwLimit = switch (dipSwitch1.substring(5)) { + case "000" -> 10_000; + case "100" -> 13_000; + case "010" -> 16_000; + case "110" -> 20_000; + case "001" -> 25_000; + case "101" -> 32_000; + default -> null; + }; this.parent.channel(EvcsKebaKeContact.ChannelId.DIP_SWITCH_MAX_HW).setNextValue(hwLimit); } @@ -383,20 +395,20 @@ protected static String hexStringToBinaryString(String dipSwitches) { return binaryString; } - private void set(EvcsKebaKeContact.ChannelId channelId, Object value) { + private void set(ChannelId channelId, Object value) { this.parent.channel(channelId).setNextValue(value); } - private void setString(EvcsKebaKeContact.ChannelId channelId, JsonObject jMessage, String name) { - this.set(channelId, JsonUtils.getAsOptionalString(jMessage, name).orElse(null)); + private void setString(ChannelId channelId, JsonObject jMessage, String name) { + this.set(channelId, getAsOptionalString(jMessage, name).orElse(null)); } - private void setInt(EvcsKebaKeContact.ChannelId channelId, JsonObject jMessage, String name) { - this.set(channelId, JsonUtils.getAsOptionalInt(jMessage, name).orElse(null)); + private void setInt(ChannelId channelId, JsonObject jMessage, String name) { + this.set(channelId, getAsOptionalInt(jMessage, name).orElse(null)); } - private void setBoolean(EvcsKebaKeContact.ChannelId channelId, JsonObject jMessage, String name) { - var enableSysOpt = JsonUtils.getAsOptionalInt(jMessage, name); + private void setBoolean(ChannelId channelId, JsonObject jMessage, String name) { + var enableSysOpt = getAsOptionalInt(jMessage, name); if (enableSysOpt.isPresent()) { this.set(channelId, enableSysOpt.get() == 1); } else { diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java index 14c23d0e9c7..e5c078d0a27 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java @@ -80,27 +80,46 @@ protected void forever() throws InterruptedException { @Override protected int getCycleTime() { // get minimum required time till next report - var now = LocalDateTime.now(); - if (this.lastReport1.isBefore(now.minusSeconds(Report.REPORT1.getRequestSeconds())) - || this.lastReport2.isBefore(now.minusSeconds(Report.REPORT2.getRequestSeconds())) - || this.lastReport3.isBefore(now.minusSeconds(Report.REPORT3.getRequestSeconds()))) { + return getCycleTimeLogic(this.lastReport1, this.lastReport2, this.lastReport3, LocalDateTime.now()); + } + + /** + * Calculates the cycletime for given dateTimes. + * + * @param lastReport1 last time report 1 was read + * @param lastReport2 last time report 2 was read + * @param lastReport3 last time report 3 was read + * @param now current time + * @return the time until the next cycle + */ + public static int getCycleTimeLogic(LocalDateTime lastReport1, LocalDateTime lastReport2, LocalDateTime lastReport3, + LocalDateTime now) { + if (lastReport1.isBefore(now.minusSeconds(Report.REPORT1.getRequestSeconds())) + || lastReport2.isBefore(now.minusSeconds(Report.REPORT2.getRequestSeconds())) + || lastReport3.isBefore(now.minusSeconds(Report.REPORT3.getRequestSeconds()))) { return 0; } - var tillReport1 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT1.getRequestSeconds()), - this.lastReport1); - var tillReport2 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT2.getRequestSeconds()), - this.lastReport2); - var tillReport3 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT3.getRequestSeconds()), - this.lastReport3); - var min = Math.min(Math.min(tillReport1, tillReport2), tillReport3); - if (min < 0) { + try { + var tillReport1 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT1.getRequestSeconds()), + lastReport1); + var tillReport2 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT2.getRequestSeconds()), + lastReport2); + var tillReport3 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT3.getRequestSeconds()), + lastReport3); + var min = Math.min(Math.min(tillReport1, tillReport2), tillReport3); + if (min < 0) { + return 0; + } + if (min > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) min; + } + } catch (ArithmeticException e) { + // if difference in tillReportX is too large a longOverflow might be thrown return 0; } - if (min > Integer.MAX_VALUE) { - return Integer.MAX_VALUE; - } else { - return (int) min; - } + } @Override diff --git a/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImplTest.java b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImplTest.java index 68b6c75ecd2..db60737ebb8 100644 --- a/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImplTest.java +++ b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImplTest.java @@ -1,27 +1,143 @@ package io.openems.edge.evcs.keba.kecontact; +import static io.openems.edge.evcs.api.PhaseRotation.L2_L3_L1; +import static io.openems.edge.evcs.api.Status.CHARGING_REJECTED; +import static io.openems.edge.evcs.keba.kecontact.Plug.PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED; + import org.junit.Test; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.keba.kecontact.core.EvcsKebaKeContactCoreImpl; import io.openems.edge.evcs.test.DummyEvcsPower; +import io.openems.edge.meter.api.ElectricityMeter; public class EvcsKebaKeContactImplTest { - private static final String COMPONENT_ID = "evcs0"; - @Test public void test() throws Exception { - new ComponentTest(new EvcsKebaKeContactImpl()) // + var sut = new EvcsKebaKeContactImpl(); + var rh = sut.readHandler; + new ComponentTest(sut) // .addReference("evcsPower", new DummyEvcsPower()) // .addReference("kebaKeContactCore", new EvcsKebaKeContactCoreImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("evcs0") // .setDebugMode(false) // .setIp("172.0.0.1") // .setMinHwCurrent(6000) // + .setPhaseRotation(L2_L3_L1) // .setUseDisplay(false) // - .build()); // + .build()) // + + .next(new TestCase() // + .onBeforeProcessImage(() -> rh.accept(REPORT_1)) // + .output(EvcsKebaKeContact.ChannelId.SERIAL, "12345678") // + .output(EvcsKebaKeContact.ChannelId.FIRMWARE, "P30 v 3.10.57 (240521-093236)") // + .output(EvcsKebaKeContact.ChannelId.COM_MODULE, "0") // + .output(EvcsKebaKeContact.ChannelId.DIP_SWITCH_1, "00100101") // + .output(EvcsKebaKeContact.ChannelId.DIP_SWITCH_2, "00000010") // + .output(EvcsKebaKeContact.ChannelId.PRODUCT, "KC-P30-EC240422-E00")) // + + .next(new TestCase() // + .onBeforeProcessImage(() -> rh.accept(REPORT_2)) // + .output(EvcsKebaKeContact.ChannelId.STATUS_KEBA, CHARGING_REJECTED) // + .output(EvcsKebaKeContact.ChannelId.ERROR_1, 0) // + .output(EvcsKebaKeContact.ChannelId.ERROR_2, 0) // + .output(EvcsKebaKeContact.ChannelId.PLUG, PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED) // + .output(EvcsKebaKeContact.ChannelId.ENABLE_SYS, false) // + .output(EvcsKebaKeContact.ChannelId.ENABLE_USER, false) // + .output(EvcsKebaKeContact.ChannelId.MAX_CURR_PERCENT, 1_000) // + .output(EvcsKebaKeContact.ChannelId.CURR_FAILSAFE, 0) // + .output(EvcsKebaKeContact.ChannelId.TIMEOUT_FAILSAFE, 0) // + .output(EvcsKebaKeContact.ChannelId.CURR_TIMER, 0) // + .output(EvcsKebaKeContact.ChannelId.TIMEOUT_CT, 0) // + .output(EvcsKebaKeContact.ChannelId.OUTPUT, false) // + .output(EvcsKebaKeContact.ChannelId.INPUT, false) // + .output(EvcsKebaKeContact.ChannelId.MAX_CURR, 32_000) // + .output(EvcsKebaKeContact.ChannelId.CURR_USER, 1_0000)) // + + .next(new TestCase() // + .onBeforeProcessImage(() -> rh.accept(REPORT_3)) // + .output(ElectricityMeter.ChannelId.VOLTAGE, 227_500) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, null) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 228_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 227_000) // + .output(ElectricityMeter.ChannelId.CURRENT, 9_075) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 0) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 9_075) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, 0) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 1_866) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 0) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, 1_866) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, 0) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, 7747834L) // + .output(Evcs.ChannelId.ENERGY_SESSION, 6530) // + .output(EvcsKebaKeContact.ChannelId.COS_PHI, 905) // + + ); } + private static final String REPORT_1 = """ + { + "ID": "1", + "Product": "KC-P30-EC240422-E00", + "Serial": "12345678", + "Firmware":"P30 v 3.10.57 (240521-093236)", + "COM-module": 0, + "Backend": 0, + "timeQ": 3, + "setBoot": 0, + "DIP-Sw1": "0x25", + "DIP-Sw2": "0x02", + "Sec": 530786 + } + """; + private static final String REPORT_2 = """ + { + "ID": "2", + "State": 5, + "Error1": 0, + "Error2": 0, + "Plug": 7, + "AuthON": 0, + "Authreq": 0, + "Enable sys": 0, + "Enable user": 0, + "Max curr": 0, + "Max curr %": 1000, + "Curr HW": 32000, + "Curr user": 10000, + "Curr FS": 0, + "Tmo FS": 0, + "Curr timer": 0, + "Tmo CT": 0, + "Setenergy": 0, + "Output": 0, + "Input": 0, + "X2 phaseSwitch source": 0, + "X2 phaseSwitch": 0, + "Serial": "22054282", + "Sec": 530786 + } + """; + private static final String REPORT_3 = """ + { + "ID": "3", + "U1": 228, + "U2": 227, + "U3": 0, + "I1": 9075, + "I2": 0, + "I3": 0, + "P": 1866156, + "PF": 905, + "E pres": 65302, + "E total": 77478335, + "Serial": "22054282", + "Sec": 534926 + } + """; + } diff --git a/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/MyConfig.java b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/MyConfig.java index 005b60b872a..6920b20339f 100644 --- a/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/MyConfig.java +++ b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/MyConfig.java @@ -1,15 +1,17 @@ package io.openems.edge.evcs.keba.kecontact; import io.openems.common.test.AbstractComponentConfig; +import io.openems.edge.evcs.api.PhaseRotation; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { protected static class Builder { private String id = null; - private int minHwCurrent; private String ip; private boolean debugMode; + private int minHwCurrent; + private PhaseRotation phaseRotation; private boolean useDisplay; private Builder() { @@ -35,6 +37,11 @@ public Builder setDebugMode(boolean debugMode) { return this; } + public Builder setPhaseRotation(PhaseRotation phaseRotation) { + this.phaseRotation = phaseRotation; + return this; + } + public Builder setUseDisplay(boolean useDisplay) { this.useDisplay = useDisplay; return this; @@ -76,6 +83,11 @@ public int minHwCurrent() { return this.builder.minHwCurrent; } + @Override + public PhaseRotation phaseRotation() { + return this.builder.phaseRotation; + } + @Override public boolean useDisplay() { return this.builder.useDisplay; diff --git a/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/ReadWorkerTest.java b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/ReadWorkerTest.java new file mode 100644 index 00000000000..713c77047dc --- /dev/null +++ b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/ReadWorkerTest.java @@ -0,0 +1,72 @@ +package io.openems.edge.evcs.keba.kecontact; + +import static org.junit.Assert.assertEquals; + +import java.time.LocalDateTime; + +import org.junit.Test; + +public class ReadWorkerTest { + + private LocalDateTime now = LocalDateTime.now(); + + @Test + public void testAllReportsDue() { + LocalDateTime lastReport1 = LocalDateTime.MIN; + LocalDateTime lastReport2 = LocalDateTime.MIN; + LocalDateTime lastReport3 = LocalDateTime.MIN; + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(0, result); + } + + @Test + public void testNoReportsDue() { + LocalDateTime lastReport1 = this.now.minusSeconds(5); + LocalDateTime lastReport2 = this.now.minusSeconds(5); + LocalDateTime lastReport3 = this.now.minusSeconds(5); + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(5000, result); + } + + @Test + public void testOneReportDue() { + LocalDateTime lastReport1 = this.now.minusHours(1); + LocalDateTime lastReport2 = this.now.minusSeconds(1); + LocalDateTime lastReport3 = this.now.minusSeconds(1); + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(0, result); + } + + @Test + public void testReportsInFuture() { + LocalDateTime lastReport1 = this.now.plusSeconds(5); + LocalDateTime lastReport2 = this.now.plusSeconds(5); + LocalDateTime lastReport3 = this.now.plusSeconds(5); + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(15000, result); + } + + @Test + public void testReportsFarInFuture() { + LocalDateTime lastReport1 = LocalDateTime.MAX; + LocalDateTime lastReport2 = LocalDateTime.MAX; + LocalDateTime lastReport3 = LocalDateTime.MAX; + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(0, result); + } + + @Test + public void testEdgeCaseReportNearNow() { + LocalDateTime lastReport1 = this.now.minusSeconds(Report.REPORT1.getRequestSeconds() - 1); + LocalDateTime lastReport2 = this.now.minusSeconds(Report.REPORT2.getRequestSeconds() - 1); + LocalDateTime lastReport3 = this.now.minusSeconds(Report.REPORT3.getRequestSeconds() - 1); + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(1000, result); + } +} \ No newline at end of file diff --git a/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/EvcsOcppAbl.java b/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/EvcsOcppAbl.java index 1cbcf9933a7..d76f92df33b 100644 --- a/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/EvcsOcppAbl.java +++ b/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/EvcsOcppAbl.java @@ -7,8 +7,10 @@ import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.MeasuringEvcs; +import io.openems.edge.meter.api.ElectricityMeter; -public interface EvcsOcppAbl extends Evcs, MeasuringEvcs, ManagedEvcs, OpenemsComponent, EventHandler { +public interface EvcsOcppAbl + extends Evcs, MeasuringEvcs, ManagedEvcs, ElectricityMeter, OpenemsComponent, EventHandler { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { ; diff --git a/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/EvcsOcppAblImpl.java b/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/EvcsOcppAblImpl.java index 30901ec28f0..d36fc398443 100644 --- a/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/EvcsOcppAblImpl.java +++ b/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/EvcsOcppAblImpl.java @@ -25,6 +25,7 @@ import eu.chargetime.ocpp.model.core.DataTransferRequest; import eu.chargetime.ocpp.model.remotetrigger.TriggerMessageRequest; import eu.chargetime.ocpp.model.remotetrigger.TriggerMessageRequestType; +import io.openems.common.types.MeterType; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; @@ -38,6 +39,7 @@ import io.openems.edge.evcs.ocpp.common.OcppInformations; import io.openems.edge.evcs.ocpp.common.OcppProfileType; import io.openems.edge.evcs.ocpp.common.OcppStandardRequests; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.timedata.api.Timedata; @Designate(ocd = Config.class, factory = true) @@ -51,7 +53,7 @@ EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE // }) public class EvcsOcppAblImpl extends AbstractManagedOcppEvcsComponent - implements EvcsOcppAbl, Evcs, MeasuringEvcs, ManagedEvcs, OpenemsComponent, EventHandler { + implements EvcsOcppAbl, Evcs, MeasuringEvcs, ManagedEvcs, ElectricityMeter, OpenemsComponent, EventHandler { // Default value for the hardware limit private static final Integer DEFAULT_HARDWARE_LIMIT = 22080; @@ -86,8 +88,8 @@ public EvcsOcppAblImpl() { super(// PROFILE_TYPES, // OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // Evcs.ChannelId.values(), // - AbstractManagedOcppEvcsComponent.ChannelId.values(), // ManagedEvcs.ChannelId.values(), // MeasuringEvcs.ChannelId.values(), // EvcsOcppAbl.ChannelId.values() // @@ -128,6 +130,11 @@ public void handleEvent(Event event) { super.handleEvent(event); } + @Override + public MeterType getMeterType() { + return MeterType.MANAGED_CONSUMPTION_METERED; + } + @Override public OcppStandardRequests getStandardRequests() { AbstractManagedOcppEvcsComponent evcs = this; diff --git a/io.openems.edge.evcs.ocpp.abl/test/io/openems/edge/evcs/ocpp/abl/EvcsOcppAblImplTest.java b/io.openems.edge.evcs.ocpp.abl/test/io/openems/edge/evcs/ocpp/abl/EvcsOcppAblImplTest.java index c3dc9a84329..13123f22149 100644 --- a/io.openems.edge.evcs.ocpp.abl/test/io/openems/edge/evcs/ocpp/abl/EvcsOcppAblImplTest.java +++ b/io.openems.edge.evcs.ocpp.abl/test/io/openems/edge/evcs/ocpp/abl/EvcsOcppAblImplTest.java @@ -8,15 +8,13 @@ public class EvcsOcppAblImplTest { - private static final String COMPONENT_ID = "evcs0"; - @Test public void test() throws Exception { new ComponentTest(new EvcsOcppAblImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addReference("evcsPower", new DummyEvcsPower()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("evcs0") // .setConnectorId(0) // .setOcppId("") // .setLogicalId("") // diff --git a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/AbstractManagedOcppEvcsComponent.java b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/AbstractManagedOcppEvcsComponent.java index f4724d48eff..5096e9b3588 100644 --- a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/AbstractManagedOcppEvcsComponent.java +++ b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/AbstractManagedOcppEvcsComponent.java @@ -1,5 +1,7 @@ package io.openems.edge.evcs.ocpp.common; +import static io.openems.edge.common.type.TypeUtils.getAsType; + import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -20,14 +22,13 @@ import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Channel; -import io.openems.edge.common.channel.Doc; import io.openems.edge.common.event.EdgeEventConstants; -import io.openems.edge.common.type.TypeUtils; import io.openems.edge.evcs.api.AbstractManagedEvcsComponent; import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.MeasuringEvcs; import io.openems.edge.evcs.api.Status; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.timedata.api.TimedataProvider; /** @@ -59,7 +60,7 @@ * */ public abstract class AbstractManagedOcppEvcsComponent extends AbstractManagedEvcsComponent - implements Evcs, ManagedEvcs, MeasuringEvcs, EventHandler, TimedataProvider { + implements Evcs, ManagedEvcs, MeasuringEvcs, ElectricityMeter, EventHandler, TimedataProvider { private final Logger log = LoggerFactory.getLogger(AbstractManagedOcppEvcsComponent.class); @@ -83,20 +84,6 @@ protected AbstractManagedOcppEvcsComponent(OcppProfileType[] profileTypes, this.profileTypes = new HashSet<>(Arrays.asList(profileTypes)); } - 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; - } - } - @Override protected void activate(ComponentContext context, String id, String alias, boolean enabled) { super.activate(context, id, alias, enabled); @@ -114,11 +101,10 @@ protected void modified(ComponentContext context, String id, String alias, boole private void setInitialSettings() { // Normally the limits are set automatically when the phase channel is set, but // not every OCPP charger provides the information about the number of phases. - int fixedMaximum = this.getFixedMaximumHardwarePower().orElse(DEFAULT_MAXIMUM_HARDWARE_POWER); - int fixedMinimum = this.getFixedMinimumHardwarePower().orElse(DEFAULT_MINIMUM_HARDWARE_POWER); - - this.getMaximumHardwarePowerChannel().setNextValue(fixedMaximum); - this.getMinimumHardwarePowerChannel().setNextValue(fixedMinimum); + this.getMaximumHardwarePowerChannel().setNextValue(// + this.getFixedMaximumHardwarePower().orElse(DEFAULT_MAXIMUM_HARDWARE_POWER)); + this.getMinimumHardwarePowerChannel().setNextValue(// + this.getFixedMinimumHardwarePower().orElse(DEFAULT_MINIMUM_HARDWARE_POWER)); } @Override @@ -157,22 +143,22 @@ private void setInitialTotalEnergyFromTimedata() { if (timedata == null || componentId == null) { return; } else { - timedata.getLatestValue(new ChannelAddress(componentId, Evcs.ChannelId.ACTIVE_CONSUMPTION_ENERGY.id())) + timedata.getLatestValue( + new ChannelAddress(componentId, ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY.id())) .thenAccept(totalEnergyOpt -> { - if (this.getActiveConsumptionEnergy().isDefined()) { + if (this.getActiveProductionEnergy().isDefined()) { // Value has been read from device in the meantime return; } if (totalEnergyOpt.isPresent()) { try { - this._setActiveConsumptionEnergy( - TypeUtils.getAsType(OpenemsType.LONG, totalEnergyOpt.get())); + this._setActiveProductionEnergy(getAsType(OpenemsType.LONG, totalEnergyOpt.get())); } catch (IllegalArgumentException e) { - this._setActiveConsumptionEnergy(TypeUtils.getAsType(OpenemsType.LONG, 0L)); + this._setActiveProductionEnergy(getAsType(OpenemsType.LONG, 0L)); } } else { - this._setActiveConsumptionEnergy(TypeUtils.getAsType(OpenemsType.LONG, 0L)); + this._setActiveConsumptionEnergy(getAsType(OpenemsType.LONG, 0L)); } }); } @@ -284,7 +270,7 @@ private void resetMeasuredChannelValues() { Channel channel = this.channel(c); channel.setNextValue(null); } - this._setChargePower(0); + this._setActivePower(0); } /** @@ -305,7 +291,7 @@ private void checkCurrentState() { case NOT_READY_FOR_CHARGING: case STARTING: case UNDEFINED: - this._setChargePower(0); + this._setActivePower(0); break; } } @@ -338,11 +324,9 @@ protected void logWarn(Logger log, String message) { @Override public String debugLog() { - if (this instanceof ManagedEvcs) { - return "Limit:" + ((ManagedEvcs) this).getSetChargePowerLimit().orElse(null) + "|" - + this.getStatus().getName(); - } - return "Power:" + this.getChargePower().orElse(0) + "|" + this.getStatus().getName(); + return "P:" + this.getActivePower().orElse(null) // + + "|Limit:" + this.getSetChargePowerLimit().orElse(null) // + + "|" + this.getStatus().getName(); } @Override diff --git a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppInformations.java b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppInformations.java index f4f4d7be976..6751117b3fb 100644 --- a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppInformations.java +++ b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppInformations.java @@ -1,9 +1,9 @@ package io.openems.edge.evcs.ocpp.common; import io.openems.edge.common.channel.ChannelId; -import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.MeasuringEvcs; import io.openems.edge.evcs.api.SocEvcs; +import io.openems.edge.meter.api.ElectricityMeter; public enum OcppInformations { @@ -95,7 +95,7 @@ public enum OcppInformations { * UnitOfMeasure for frequency, the UnitOfMeasure for any SampledValue with * measurand: Frequency is Hertz. */ - CORE_METER_VALUES_FREQUENCY("Frequency", MeasuringEvcs.ChannelId.FREQUENCY), + CORE_METER_VALUES_FREQUENCY("Frequency", ElectricityMeter.ChannelId.FREQUENCY), /** * Instantaneous active power exported by EV. (W) @@ -105,7 +105,7 @@ public enum OcppInformations { /** * Instantaneous active power imported by EV. (W) */ - CORE_METER_VALUES_POWER_ACTIVE_IMPORT("Power.Active.Import", Evcs.ChannelId.CHARGE_POWER), + CORE_METER_VALUES_POWER_ACTIVE_IMPORT("Power.Active.Import", ElectricityMeter.ChannelId.ACTIVE_POWER), /** * Instantaneous power factor of total energy flow. @@ -146,7 +146,7 @@ public enum OcppInformations { /** * Instantaneous AC RMS supply voltage. */ - CORE_METER_VALUES_VOLTAGE("Voltage", MeasuringEvcs.ChannelId.VOLTAGE); + CORE_METER_VALUES_VOLTAGE("Voltage", ElectricityMeter.ChannelId.VOLTAGE); private final String ocppValue; private final ChannelId channelId; diff --git a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingle.java b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingle.java index fed8dedd6f2..4f3ac815d89 100644 --- a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingle.java +++ b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingle.java @@ -8,9 +8,10 @@ import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.MeasuringEvcs; import io.openems.edge.evcs.api.SocEvcs; +import io.openems.edge.meter.api.ElectricityMeter; public interface EvcsOcppIesKeywattSingle - extends Evcs, ManagedEvcs, MeasuringEvcs, OpenemsComponent, EventHandler, SocEvcs { + extends Evcs, ManagedEvcs, MeasuringEvcs, ElectricityMeter, OpenemsComponent, EventHandler, SocEvcs { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { ; diff --git a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingleImpl.java b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingleImpl.java index b27dde46b05..b344667a3ae 100644 --- a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingleImpl.java +++ b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingleImpl.java @@ -23,6 +23,7 @@ import eu.chargetime.ocpp.model.Request; import eu.chargetime.ocpp.model.core.ChangeConfigurationRequest; +import io.openems.common.types.MeterType; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; @@ -36,6 +37,7 @@ import io.openems.edge.evcs.ocpp.common.OcppInformations; import io.openems.edge.evcs.ocpp.common.OcppProfileType; import io.openems.edge.evcs.ocpp.common.OcppStandardRequests; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.timedata.api.Timedata; @Designate(ocd = Config.class, factory = true) @@ -48,8 +50,8 @@ EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, // EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE // }) -public class EvcsOcppIesKeywattSingleImpl extends AbstractManagedOcppEvcsComponent - implements EvcsOcppIesKeywattSingle, Evcs, ManagedEvcs, MeasuringEvcs, OpenemsComponent, EventHandler, SocEvcs { +public class EvcsOcppIesKeywattSingleImpl extends AbstractManagedOcppEvcsComponent implements EvcsOcppIesKeywattSingle, + Evcs, ManagedEvcs, MeasuringEvcs, ElectricityMeter, OpenemsComponent, EventHandler, SocEvcs { // Profiles that a Ies KeyWatt is supporting private static final OcppProfileType[] PROFILE_TYPES = { // @@ -76,8 +78,8 @@ public EvcsOcppIesKeywattSingleImpl() { super(// PROFILE_TYPES, // OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // Evcs.ChannelId.values(), // - AbstractManagedOcppEvcsComponent.ChannelId.values(), // ManagedEvcs.ChannelId.values(), // MeasuringEvcs.ChannelId.values(), // SocEvcs.ChannelId.values(), // @@ -102,6 +104,11 @@ protected void deactivate() { super.deactivate(); } + @Override + public MeterType getMeterType() { + return MeterType.MANAGED_CONSUMPTION_METERED; + } + private void setInitalSettings(Config config) { this.config = config; this._setPowerPrecision(1); diff --git a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/test/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingleImplTest.java b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/test/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingleImplTest.java index 8d0bea95587..313bd8288ff 100644 --- a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/test/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingleImplTest.java +++ b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/test/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/EvcsOcppIesKeywattSingleImplTest.java @@ -8,15 +8,13 @@ public class EvcsOcppIesKeywattSingleImplTest { - private static final String COMPONENT_ID = "evcs0"; - @Test public void test() throws Exception { new ComponentTest(new EvcsOcppIesKeywattSingleImpl()) // .addReference("componentManager", new DummyComponentManager()) // .addReference("evcsPower", new DummyEvcsPower()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("evcs0") // .setConnectorId(0) // .setOcppId("") // .setDebugMode(false) // diff --git a/io.openems.edge.evcs.ocpp.server/bnd.bnd b/io.openems.edge.evcs.ocpp.server/bnd.bnd index 500e91981af..da432b59483 100644 --- a/io.openems.edge.evcs.ocpp.server/bnd.bnd +++ b/io.openems.edge.evcs.ocpp.server/bnd.bnd @@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.common,\ io.openems.edge.evcs.api,\ io.openems.edge.evcs.ocpp.common,\ + io.openems.edge.meter.api,\ io.openems.edge.timedata.api,\ io.openems.wrapper.eu.chargetime.ocpp,\ diff --git a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/CoreEventHandlerImpl.java b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/CoreEventHandlerImpl.java index 1342ff71913..5189a6ffe41 100644 --- a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/CoreEventHandlerImpl.java +++ b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/CoreEventHandlerImpl.java @@ -121,7 +121,7 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter */ var format = value.getFormat(); if (format.equals(ValueFormat.SignedData)) { - val = this.fromHexToDezString(val); + val = fromHexToDezString(val); } var measurand = OcppInformations @@ -143,14 +143,14 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter case CORE_METER_VALUES_ENERGY_ACTIVE_IMPORT_INTERVAL: case CORE_METER_VALUES_ENERGY_ACTIVE_EXPORT_INTERVAL: if (unit.equals(Unit.KWH)) { - val = this.multipliedByThousand(val); + val = multipliedByThousand(val); } correctValue = Double.valueOf(val); break; case CORE_METER_VALUES_ENERGY_ACTIVE_IMPORT_REGISTER: if (unit.equals(Unit.KWH)) { - val = this.multipliedByThousand(val); + val = multipliedByThousand(val); } correctValue = Double.valueOf(val); @@ -165,13 +165,12 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter break; } - var sessionEnergy = 0; - var totalEnergy = 0L; - /* * Calculating the energy in this session and in total for the given energy * value. */ + final int sessionEnergy; + final long totalEnergy; if (evcs.returnsSessionEnergy()) { sessionEnergy = (int) energy; totalEnergy = evcs.getSessionStart().getEnergy() + energy; @@ -180,7 +179,7 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter totalEnergy = energy; } evcs._setEnergySession(sessionEnergy); - evcs._setActiveConsumptionEnergy(totalEnergy); + evcs._setActiveProductionEnergy(totalEnergy); break; case CORE_METER_VALUES_ENERGY_REACTIVE_EXPORT_REGISTER: @@ -188,7 +187,7 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter case CORE_METER_VALUES_ENERGY_REACTIVE_EXPORT_INTERVAL: case CORE_METER_VALUES_ENERGY_REACTIVE_IMPORT_INTERVAL: if (unit.equals(Unit.KVARH)) { - val = this.multipliedByThousand(val); + val = multipliedByThousand(val); } correctValue = Double.valueOf(val); break; @@ -197,7 +196,7 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter case CORE_METER_VALUES_POWER_ACTIVE_IMPORT: case CORE_METER_VALUES_POWER_OFFERED: if (unit.equals(Unit.KW)) { - val = this.multipliedByThousand(val); + val = multipliedByThousand(val); } correctValue = (int) Math.round(Double.parseDouble(val)); @@ -226,7 +225,7 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter case CORE_METER_VALUES_POWER_REACTIVE_EXPORT: case CORE_METER_VALUES_POWER_REACTIVE_IMPORT: if (unit.equals(Unit.KVAR)) { - val = this.multipliedByThousand(val); + val = multipliedByThousand(val); } correctValue = (int) Math.round(Double.parseDouble(val)); break; @@ -277,8 +276,9 @@ public StatusNotificationConfirmation handleStatusNotificationRequest(UUID sessi evcs.getSessionEnd().resetChargeSessionStampIfPresent(); // Set the start charge session stamp - evcs.getSessionStart().setChargeSessionStampIfNotPresent( - Instant.now(this.parent.componentManager.getClock()), evcs.getActiveConsumptionEnergy().orElse(0L)); + evcs.getSessionStart().setChargeSessionStampIfNotPresent(// + Instant.now(this.parent.componentManager.getClock()), // + evcs.getActiveProductionEnergy().orElse(0L)); break; case Faulted: evcsStatus = Status.ERROR; @@ -289,8 +289,9 @@ public StatusNotificationConfirmation handleStatusNotificationRequest(UUID sessi // Reset the start charge session stamp evcs.getSessionStart().resetChargeSessionStampIfPresent(); - evcs.getSessionEnd().setChargeSessionStampIfNotPresent(Instant.now(this.parent.componentManager.getClock()), - evcs.getActiveConsumptionEnergy().orElse(0L)); + evcs.getSessionEnd().setChargeSessionStampIfNotPresent(// + Instant.now(this.parent.componentManager.getClock()), // + evcs.getActiveProductionEnergy().orElse(0L)); break; case Preparing: evcsStatus = Status.READY_FOR_CHARGING; @@ -403,7 +404,7 @@ private AbstractManagedOcppEvcsComponent getEvcsBySessionIndexAndConnector(UUID * @param hex given value in hex * @return Decimal value as String */ - public String fromHexToDezString(String hex) { + public static String fromHexToDezString(String hex) { var dezValue = Integer.parseInt(hex, 16); return String.valueOf(dezValue); } @@ -414,7 +415,7 @@ public String fromHexToDezString(String hex) { * @param val value * @return Value / 1000 as String */ - private String multipliedByThousand(String val) { + private static String multipliedByThousand(String val) { if (val.isEmpty()) { return val; } @@ -435,8 +436,8 @@ private void setPowerDependingOnEnergy(AbstractManagedOcppEvcsComponent evcs, Do var power = 0; if (lastChargingProperty != null) { - power = this.calculateChargePower(lastChargingProperty, currentEnergy, timestamp); - evcs._setChargePower(power); + power = this.calculateActivePower(lastChargingProperty, currentEnergy, timestamp); + evcs._setActivePower(power); } evcs.setLastChargingProperty(new ChargingProperty(power, currentEnergy, timestamp)); } @@ -449,7 +450,7 @@ private void setPowerDependingOnEnergy(AbstractManagedOcppEvcsComponent evcs, Do * @param timestamp Time when the current Energy was measured. * @return current power */ - private int calculateChargePower(ChargingProperty lastMeterValue, double currentEnergy, ZonedDateTime timestamp) { + private int calculateActivePower(ChargingProperty lastMeterValue, double currentEnergy, ZonedDateTime timestamp) { double diffseconds = Duration.between(timestamp, lastMeterValue.getTimestamp()).getSeconds(); diff --git a/io.openems.edge.evcs.spelsberg/bnd.bnd b/io.openems.edge.evcs.spelsberg/bnd.bnd index 352d8d2c11e..be0c8b9d00b 100644 --- a/io.openems.edge.evcs.spelsberg/bnd.bnd +++ b/io.openems.edge.evcs.spelsberg/bnd.bnd @@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.bridge.modbus,\ io.openems.edge.common,\ io.openems.edge.evcs.api,\ + io.openems.edge.meter.api,\ -testpath: \ ${testpath} \ No newline at end of file diff --git a/io.openems.edge.evcs.spelsberg/src/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmart.java b/io.openems.edge.evcs.spelsberg/src/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmart.java index 4b05ab4ca21..5e16a5ffacf 100644 --- a/io.openems.edge.evcs.spelsberg/src/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmart.java +++ b/io.openems.edge.evcs.spelsberg/src/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmart.java @@ -64,30 +64,6 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .accessMode(AccessMode.READ_ONLY) // .text("Maximum current signaled to the EV for charging")), - CURRENT_L1(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE).accessMode(AccessMode.READ_ONLY) // - .text("Current on L1")), - - CURRENT_L2(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE).accessMode(AccessMode.READ_ONLY) // - .text("Current on L2")), - - CURRENT_L3(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE).accessMode(AccessMode.READ_ONLY) // - .text("Current on L3")), - - POWER_L1(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT).accessMode(AccessMode.READ_ONLY) // - .text("Charging power on L1")), - - POWER_L2(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT).accessMode(AccessMode.READ_ONLY) // - .text("Charging power on L2")), - - POWER_L3(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT).accessMode(AccessMode.READ_ONLY) // - .text("Charging power on L3")), - POWER_TOTAL(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT).accessMode(AccessMode.READ_ONLY) // .text("Sum of active charging power")), @@ -132,60 +108,6 @@ public default void setApplyChargePowerLimit(Integer value) throws OpenemsNamedE this.getApplyChargePowerLimitChannel().setNextWriteValue(value); } - /** - * Gets the Channel for {@link ChannelId#POWER_L1}. - * - * @return the Channel - */ - public default Channel getChargePowerL1Channel() { - return this.channel(ChannelId.POWER_L1); - } - - /** - * Gets the Power on phase L1 in [W]. See {@link ChannelId#POWER_L1}. - * - * @return the Channel {@link Value} - */ - public default Value getChargePowerL1() { - return this.getChargePowerL1Channel().value(); - } - - /** - * Gets the Channel for {@link ChannelId#POWER_L2}. - * - * @return the Channel - */ - public default Channel getChargePowerL2Channel() { - return this.channel(ChannelId.POWER_L2); - } - - /** - * Gets the Power on phase L2 in [W]. See {@link ChannelId#POWER_L2}. - * - * @return the Channel {@link Value} - */ - public default Value getChargePowerL2() { - return this.getChargePowerL2Channel().value(); - } - - /** - * Gets the Channel for {@link ChannelId#POWER_L3}. - * - * @return the Channel - */ - public default Channel getChargePowerL3Channel() { - return this.channel(ChannelId.POWER_L3); - } - - /** - * Gets the Power on phase L3 in [W]. See {@link ChannelId#POWER_L3}. - * - * @return the Channel {@link Value} - */ - public default Value getChargePowerL3() { - return this.getChargePowerL3Channel().value(); - } - /** * Gets the Channel for {@link ChannelId#POWER_TOTAL}. * diff --git a/io.openems.edge.evcs.spelsberg/src/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmartImpl.java b/io.openems.edge.evcs.spelsberg/src/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmartImpl.java index 71e6df3342a..e8901fc7d8c 100644 --- a/io.openems.edge.evcs.spelsberg/src/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmartImpl.java +++ b/io.openems.edge.evcs.spelsberg/src/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmartImpl.java @@ -22,6 +22,7 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -44,6 +45,7 @@ import io.openems.edge.evcs.api.Phases; import io.openems.edge.evcs.api.Status; import io.openems.edge.evcs.api.WriteHandler; +import io.openems.edge.meter.api.ElectricityMeter; @Designate(ocd = Config.class, factory = true) @Component(// @@ -54,8 +56,8 @@ @EventTopics({ // EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE // }) -public class EvcsSpelsbergSmartImpl extends AbstractOpenemsModbusComponent - implements EvcsSpelsbergSmart, Evcs, ManagedEvcs, ModbusComponent, OpenemsComponent, EventHandler { +public class EvcsSpelsbergSmartImpl extends AbstractOpenemsModbusComponent implements EvcsSpelsbergSmart, Evcs, + ManagedEvcs, ModbusComponent, OpenemsComponent, EventHandler, ElectricityMeter { private final Logger log = LoggerFactory.getLogger(EvcsSpelsbergSmartImpl.class); private final ChargeStateHandler chargeStateHandler = new ChargeStateHandler(this); @@ -77,6 +79,7 @@ protected void setModbus(BridgeModbus modbus) { public EvcsSpelsbergSmartImpl() { super(// OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // ModbusComponent.ChannelId.values(), // Evcs.ChannelId.values(), // ManagedEvcs.ChannelId.values(), // @@ -127,23 +130,24 @@ protected ModbusProtocol defineModbusProtocol() { m(EvcsSpelsbergSmart.ChannelId.CABLE_STATE, new UnsignedWordElement(1004))), new FC3ReadRegistersTask(1008, Priority.LOW, - m(EvcsSpelsbergSmart.ChannelId.CURRENT_L1, new UnsignedWordElement(1008)), + m(ElectricityMeter.ChannelId.CURRENT_L1, new UnsignedWordElement(1008)), new DummyRegisterElement(1009), - m(EvcsSpelsbergSmart.ChannelId.CURRENT_L2, new UnsignedWordElement(1010)), + m(ElectricityMeter.ChannelId.CURRENT_L2, new UnsignedWordElement(1010)), new DummyRegisterElement(1011), - m(EvcsSpelsbergSmart.ChannelId.CURRENT_L3, new UnsignedWordElement(1012))), + m(ElectricityMeter.ChannelId.CURRENT_L3, new UnsignedWordElement(1012))), new FC3ReadRegistersTask(1020, Priority.HIGH, - m(Evcs.ChannelId.CHARGE_POWER, new UnsignedDoublewordElement(1020)), + m(ElectricityMeter.ChannelId.ACTIVE_POWER, new UnsignedDoublewordElement(1020)), + // TODO whats the difference between 1020 and 1022? m(EvcsSpelsbergSmart.ChannelId.POWER_TOTAL, new UnsignedDoublewordElement(1022)), - m(EvcsSpelsbergSmart.ChannelId.POWER_L1, new UnsignedDoublewordElement(1024)), + m(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, new UnsignedDoublewordElement(1024)), new DummyRegisterElement(1026, 1027), - m(EvcsSpelsbergSmart.ChannelId.POWER_L2, new UnsignedDoublewordElement(1028)), + m(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, new UnsignedDoublewordElement(1028)), new DummyRegisterElement(1030, 1031), - m(EvcsSpelsbergSmart.ChannelId.POWER_L3, new UnsignedDoublewordElement(1032))), + m(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, new UnsignedDoublewordElement(1032))), new FC3ReadRegistersTask(1036, Priority.LOW, - m(Evcs.ChannelId.ACTIVE_CONSUMPTION_ENERGY, new UnsignedDoublewordElement(1036))), + m(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, new UnsignedDoublewordElement(1036))), new FC3ReadRegistersTask(1100, Priority.LOW, m(EvcsSpelsbergSmart.ChannelId.MAX_HARDWARE_CURRENT, new UnsignedWordElement(1100)), @@ -181,6 +185,11 @@ protected ModbusProtocol defineModbusProtocol() { return modbusProtocol; } + @Override + public MeterType getMeterType() { + return MeterType.MANAGED_CONSUMPTION_METERED; + } + @Override public String debugLog() { return "Status: " + getStatus().getName() + " | " + "Charging Power: " + getChargePowerTotal(); @@ -290,21 +299,13 @@ private void addStatusCallback() { */ private void addPhaseDetectionCallback() { final Consumer> setPhasesCallback = ignore -> { - - var phases = 0; - if (this.getChargePowerL1().isDefined() && this.getChargePowerL1().get() > 0) { - phases++; - } - if (this.getChargePowerL2().isDefined() && this.getChargePowerL2().get() > 0) { - phases++; - } - if (this.getChargePowerL3().isDefined() && this.getChargePowerL3().get() > 0) { - phases++; - } - - this._setPhases(phases); + this._setPhases(Evcs.evaluatePhaseCount(// + this.getActivePowerL1().get(), // + this.getActivePowerL2().get(), // + this.getActivePowerL3().get())); }; + // TODO remove this channel this.getChargePowerTotalChannel().onUpdate(setPhasesCallback); } diff --git a/io.openems.edge.evcs.spelsberg/test/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmartImplTest.java b/io.openems.edge.evcs.spelsberg/test/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmartImplTest.java index c1b4be1ea43..61854c8a834 100644 --- a/io.openems.edge.evcs.spelsberg/test/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmartImplTest.java +++ b/io.openems.edge.evcs.spelsberg/test/io/openems/edge/evcs/spelsberg/smart/EvcsSpelsbergSmartImplTest.java @@ -8,17 +8,14 @@ public class EvcsSpelsbergSmartImplTest { - private static final String EVCS_ID = "evcs0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new EvcsSpelsbergSmartImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setModbusId(MODBUS_ID) // - .setId(EVCS_ID) // + .setModbusId("modbus0") // + .setId("evcs0") // .setModbusUnitId(255) // .setMaxHwCurrent(16000) // .setMinHwCurrent(6000) // diff --git a/io.openems.edge.evcs.webasto.next/bnd.bnd b/io.openems.edge.evcs.webasto.next/bnd.bnd index d8f6c880de2..dedafbd9bf3 100644 --- a/io.openems.edge.evcs.webasto.next/bnd.bnd +++ b/io.openems.edge.evcs.webasto.next/bnd.bnd @@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.bridge.modbus,\ io.openems.edge.common,\ io.openems.edge.evcs.api,\ + io.openems.edge.meter.api,\ -testpath: \ ${testpath} \ No newline at end of file diff --git a/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/EvcsWebastoNext.java b/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/EvcsWebastoNext.java index fe1dc68577d..06b57a37520 100644 --- a/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/EvcsWebastoNext.java +++ b/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/EvcsWebastoNext.java @@ -5,7 +5,6 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; -import io.openems.edge.common.channel.IntegerReadChannel; import io.openems.edge.common.channel.IntegerWriteChannel; import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; @@ -31,26 +30,6 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { EVSE_ERROR_CODE(Doc.of(EvseErrorCode.values())), // - CURRENT_L1(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE) // - .accessMode(AccessMode.READ_ONLY)), // - - CURRENT_L2(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE) // - .accessMode(AccessMode.READ_ONLY)), // - - CURRENT_L3(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE)), // - - POWER_L1(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT)), // - - POWER_L2(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT)), // - - POWER_L3(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT)), // - MAX_HW_CURRENT(Doc.of(OpenemsType.INTEGER) // .unit(Unit.AMPERE)), // @@ -161,58 +140,4 @@ public default Value getEvSetChargePowerLimit() { public default void setEvSetChargePowerLimit(Integer value) throws OpenemsNamedException { this.getEvSetChargePowerLimitChannel().setNextWriteValue(value); } - - /** - * Gets the Channel for {@link ChannelId#POWER_L1}. - * - * @return the Channel - */ - public default IntegerReadChannel getPowerL1Channel() { - return this.channel(ChannelId.POWER_L1); - } - - /** - * Gets the Power on phase 1 in [W]. See {@link ChannelId#POWER_L1}. - * - * @return the Channel {@link Value} - */ - public default Value getPowerL1() { - return this.getPowerL1Channel().value(); - } - - /** - * Gets the Channel for {@link ChannelId#POWER_L2}. - * - * @return the Channel - */ - public default IntegerReadChannel getPowerL2Channel() { - return this.channel(ChannelId.POWER_L2); - } - - /** - * Gets the Power on phase 2 in [W]. See {@link ChannelId#POWER_L2}. - * - * @return the Channel {@link Value} - */ - public default Value getPowerL2() { - return this.getPowerL2Channel().value(); - } - - /** - * Gets the Channel for {@link ChannelId#POWER_L3}. - * - * @return the Channel - */ - public default IntegerReadChannel getPowerL3Channel() { - return this.channel(ChannelId.POWER_L3); - } - - /** - * Gets the Power on phase 3 in [W]. See {@link ChannelId#POWER_L3}. - * - * @return the Channel {@link Value} - */ - public default Value getPowerL3() { - return this.getPowerL3Channel().value(); - } } diff --git a/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/EvcsWebastoNextImpl.java b/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/EvcsWebastoNextImpl.java index d57c72553c9..4ba75efe77b 100644 --- a/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/EvcsWebastoNextImpl.java +++ b/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/EvcsWebastoNextImpl.java @@ -22,6 +22,7 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -45,6 +46,7 @@ import io.openems.edge.evcs.api.Status; import io.openems.edge.evcs.api.WriteHandler; import io.openems.edge.evcs.webasto.next.enums.ChargePointState; +import io.openems.edge.meter.api.ElectricityMeter; @Designate(ocd = Config.class, factory = true) @Component(// @@ -55,11 +57,10 @@ @EventTopics({ // EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, // }) -public class EvcsWebastoNextImpl extends AbstractOpenemsModbusComponent - implements EvcsWebastoNext, Evcs, ManagedEvcs, ModbusComponent, OpenemsComponent, EventHandler { +public class EvcsWebastoNextImpl extends AbstractOpenemsModbusComponent implements EvcsWebastoNext, Evcs, ManagedEvcs, + ElectricityMeter, ModbusComponent, OpenemsComponent, EventHandler { private static final int DEFAULT_LIFE_BIT = 1; - private static final int DETECT_PHASE_ACTIVITY = 100; // W private final Logger log = LoggerFactory.getLogger(EvcsWebastoNext.class); @@ -84,6 +85,7 @@ protected void setModbus(BridgeModbus modbus) { public EvcsWebastoNextImpl() { super(// OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // ModbusComponent.ChannelId.values(), // Evcs.ChannelId.values(), // ManagedEvcs.ChannelId.values(), // @@ -151,21 +153,21 @@ protected ModbusProtocol defineModbusProtocol() { new FC3ReadRegistersTask(1006, Priority.LOW, m(EvcsWebastoNext.ChannelId.EVSE_ERROR_CODE, new UnsignedWordElement(1006))), new FC3ReadRegistersTask(1008, Priority.LOW, - m(EvcsWebastoNext.ChannelId.CURRENT_L1, new UnsignedWordElement(1008))), + m(ElectricityMeter.ChannelId.CURRENT_L1, new UnsignedWordElement(1008))), new FC3ReadRegistersTask(1010, Priority.LOW, - m(EvcsWebastoNext.ChannelId.CURRENT_L2, new UnsignedWordElement(1010))), + m(ElectricityMeter.ChannelId.CURRENT_L2, new UnsignedWordElement(1010))), new FC3ReadRegistersTask(1012, Priority.LOW, - m(EvcsWebastoNext.ChannelId.CURRENT_L3, new UnsignedWordElement(1012))), + m(ElectricityMeter.ChannelId.CURRENT_L3, new UnsignedWordElement(1012))), new FC3ReadRegistersTask(1020, Priority.HIGH, - m(Evcs.ChannelId.CHARGE_POWER, new UnsignedDoublewordElement(1020))), + m(ElectricityMeter.ChannelId.ACTIVE_POWER, new UnsignedDoublewordElement(1020))), new FC3ReadRegistersTask(1024, Priority.LOW, - m(EvcsWebastoNext.ChannelId.POWER_L1, new UnsignedDoublewordElement(1024))), + m(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, new UnsignedDoublewordElement(1024))), new FC3ReadRegistersTask(1028, Priority.LOW, - m(EvcsWebastoNext.ChannelId.POWER_L2, new UnsignedDoublewordElement(1028))), + m(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, new UnsignedDoublewordElement(1028))), new FC3ReadRegistersTask(1032, Priority.LOW, - m(EvcsWebastoNext.ChannelId.POWER_L3, new UnsignedDoublewordElement(1032))), + m(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, new UnsignedDoublewordElement(1032))), new FC3ReadRegistersTask(1036, Priority.LOW, - m(Evcs.ChannelId.ACTIVE_CONSUMPTION_ENERGY, new UnsignedDoublewordElement(1036))), + m(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, new UnsignedDoublewordElement(1036))), new FC3ReadRegistersTask(1100, Priority.LOW, m(EvcsWebastoNext.ChannelId.MAX_HW_CURRENT, new UnsignedWordElement(1100))), new FC3ReadRegistersTask(1102, Priority.LOW, @@ -241,25 +243,20 @@ private void addStatusListener() { } private void addPhasesListener() { - final Consumer> setPhases = ignore -> { - var phases = 0; - if (this.getPowerL1().orElse(0) > DETECT_PHASE_ACTIVITY) { - phases++; - } - if (this.getPowerL2().orElse(0) > DETECT_PHASE_ACTIVITY) { - phases++; - } - if (this.getPowerL3().orElse(0) > DETECT_PHASE_ACTIVITY) { - phases++; - } - if (phases == 0) { - phases = 3; - } - this._setPhases(phases); + final Consumer> setPhasesCallback = ignore -> { + this._setPhases(Evcs.evaluatePhaseCount(// + this.getActivePowerL1().get(), // + this.getActivePowerL2().get(), // + this.getActivePowerL3().get())); }; - this.getPowerL1Channel().onUpdate(setPhases); - this.getPowerL2Channel().onUpdate(setPhases); - this.getPowerL3Channel().onUpdate(setPhases); + this.getActivePowerL1Channel().onUpdate(setPhasesCallback); + this.getActivePowerL2Channel().onUpdate(setPhasesCallback); + this.getActivePowerL3Channel().onUpdate(setPhasesCallback); + } + + @Override + public MeterType getMeterType() { + return MeterType.MANAGED_CONSUMPTION_METERED; } @Override diff --git a/io.openems.edge.evcs.webasto.next/test/io/openems/edge/evcs/webasto/next/EvcsWebastoNextImplTest.java b/io.openems.edge.evcs.webasto.next/test/io/openems/edge/evcs/webasto/next/EvcsWebastoNextImplTest.java index b19ec679450..7ffc73c4d6a 100644 --- a/io.openems.edge.evcs.webasto.next/test/io/openems/edge/evcs/webasto/next/EvcsWebastoNextImplTest.java +++ b/io.openems.edge.evcs.webasto.next/test/io/openems/edge/evcs/webasto/next/EvcsWebastoNextImplTest.java @@ -8,17 +8,14 @@ public class EvcsWebastoNextImplTest { - private static final String EVCS_ID = "evcs0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new EvcsWebastoNextImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setModbusId(MODBUS_ID) // - .setId(EVCS_ID) // + .setModbusId("modbus0") // + .setId("evcs0") // .setModbusUnitId(1) // .setMaxHwCurrent(32000) // .setMinHwCurrent(6000) // diff --git a/io.openems.edge.evcs.webasto.unite/bnd.bnd b/io.openems.edge.evcs.webasto.unite/bnd.bnd index 2f4e7fe8a62..a4ed98727d0 100644 --- a/io.openems.edge.evcs.webasto.unite/bnd.bnd +++ b/io.openems.edge.evcs.webasto.unite/bnd.bnd @@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.bridge.modbus,\ io.openems.edge.common,\ io.openems.edge.evcs.api,\ + io.openems.edge.meter.api,\ -testpath: \ ${testpath} \ No newline at end of file diff --git a/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/EvcsWebastoUnite.java b/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/EvcsWebastoUnite.java index a7a8803ec81..1341e62a1c1 100644 --- a/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/EvcsWebastoUnite.java +++ b/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/EvcsWebastoUnite.java @@ -8,8 +8,10 @@ import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.WriteChannel; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.meter.api.ElectricityMeter; -public interface EvcsWebastoUnite extends OpenemsComponent { +public interface EvcsWebastoUnite extends ElectricityMeter, Evcs, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { SERIAL_NUMBER(Doc.of(OpenemsType.STRING) // @@ -41,36 +43,6 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .accessMode(AccessMode.READ_ONLY)), // EVSE_FAULT_CODE(Doc.of(OpenemsType.INTEGER) // .accessMode(AccessMode.READ_ONLY)), // - CURRENT_L1(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE) // - .accessMode(AccessMode.READ_ONLY)), // - CURRENT_L2(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE) // - .accessMode(AccessMode.READ_ONLY)), // - CURRENT_L3(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE) // - .accessMode(AccessMode.READ_ONLY)), // - VOLTAGE_L1(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.VOLT) // - .accessMode(AccessMode.READ_ONLY)), // - VOLTAGE_L2(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.VOLT) // - .accessMode(AccessMode.READ_ONLY)), // - VOLTAGE_L3(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.VOLT) // - .accessMode(AccessMode.READ_ONLY)), // - ACTIVE_POWER_TOTAL(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT) // - .accessMode(AccessMode.READ_ONLY)), // - ACTIVE_POWER_L1(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT) // - .accessMode(AccessMode.READ_ONLY)), // - ACTIVE_POWER_L2(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT) // - .accessMode(AccessMode.READ_ONLY)), // - ACTIVE_POWER_L3(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT) // - .accessMode(AccessMode.READ_ONLY)), // METER_READING(Doc.of(OpenemsType.INTEGER) // .accessMode(AccessMode.READ_ONLY)), // SESSION_MAX_CURRENT(Doc.of(OpenemsType.INTEGER) // @@ -133,25 +105,6 @@ default void _setAliveValue(int value) throws OpenemsError.OpenemsNamedException channel.setNextWriteValue(value); } - /** - * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#ACTIVE_POWER_TOTAL}. - * - * @return the Channel - */ - default Channel getActivePowerChannel() { - return this.channel(ChannelId.ACTIVE_POWER_TOTAL); - } - - /** - * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#ACTIVE_POWER_TOTAL}. - * - * @return the Channel - */ - default int getActivePower() { - Channel channel = this.getActivePowerChannel(); - return channel.value().orElse(channel.getNextValue().orElse(0)); - } - /** * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#CHARGE_POINT_STATE}. * @@ -171,63 +124,6 @@ default int getChargePointState() { return channel.value().orElse(channel.getNextValue().orElse(-1)); } - /** - * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#CURRENT_L1}. - * - * @return the Channel - */ - default Channel getActivePowerL1Channel() { - return this.channel(ChannelId.ACTIVE_POWER_L1); - } - - /** - * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#CURRENT_L1}. - * - * @return the Channel - */ - default int getActivePowerL1() { - Channel channel = this.getActivePowerL1Channel(); - return channel.value().orElse(channel.getNextValue().orElse(-1)); - } - - /** - * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#CURRENT_L2}. - * - * @return the Channel - */ - default Channel getActivePowerL2Channel() { - return this.channel(ChannelId.ACTIVE_POWER_L2); - } - - /** - * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#CURRENT_L2}. - * - * @return the Channel - */ - default int getActivePowerL2() { - Channel channel = this.getActivePowerL2Channel(); - return channel.value().orElse(channel.getNextValue().orElse(-1)); - } - - /** - * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#CURRENT_L3}. - * - * @return the Channel - */ - default Channel getActivePowerL3Channel() { - return this.channel(ChannelId.ACTIVE_POWER_L3); - } - - /** - * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#CURRENT_L3}. - * - * @return the Channel - */ - default int getActivePowerL3() { - Channel channel = this.getActivePowerL3Channel(); - return channel.value().orElse(channel.getNextValue().orElse(-1)); - } - /** * Gets the Channel for {@link EvcsWebastoUnite.ChannelId#CHARGING_CURRENT}. * diff --git a/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/EvcsWebastoUniteImpl.java b/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/EvcsWebastoUniteImpl.java index 8c0d5a821f6..0baaba59b30 100644 --- a/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/EvcsWebastoUniteImpl.java +++ b/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/EvcsWebastoUniteImpl.java @@ -20,6 +20,7 @@ import io.openems.common.exceptions.OpenemsError; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -41,6 +42,7 @@ import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.Phases; import io.openems.edge.evcs.api.WriteHandler; +import io.openems.edge.meter.api.ElectricityMeter; @Designate(ocd = Config.class, factory = true) @Component(// @@ -53,7 +55,7 @@ EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE // }) public class EvcsWebastoUniteImpl extends AbstractOpenemsModbusComponent - implements EvcsWebastoUnite, Evcs, ManagedEvcs, EventHandler, OpenemsComponent { + implements EvcsWebastoUnite, Evcs, ManagedEvcs, EventHandler, ElectricityMeter, OpenemsComponent { private final Logger log = LoggerFactory.getLogger(EvcsWebastoUniteImpl.class); @@ -74,6 +76,7 @@ public class EvcsWebastoUniteImpl extends AbstractOpenemsModbusComponent public EvcsWebastoUniteImpl() { super(// OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // Evcs.ChannelId.values(), // ManagedEvcs.ChannelId.values(), // ModbusComponent.ChannelId.values(), // @@ -150,25 +153,25 @@ protected ModbusProtocol defineModbusProtocol() { new FC4ReadInputRegistersTask(1006, Priority.LOW, m(EvcsWebastoUnite.ChannelId.EVSE_FAULT_CODE, new UnsignedDoublewordElement(1006))), new FC4ReadInputRegistersTask(1008, Priority.HIGH, - m(EvcsWebastoUnite.ChannelId.CURRENT_L1, new UnsignedWordElement(1008))), + m(ElectricityMeter.ChannelId.CURRENT_L1, new UnsignedWordElement(1008))), new FC4ReadInputRegistersTask(1010, Priority.HIGH, - m(EvcsWebastoUnite.ChannelId.CURRENT_L2, new UnsignedWordElement(1010))), + m(ElectricityMeter.ChannelId.CURRENT_L2, new UnsignedWordElement(1010))), new FC4ReadInputRegistersTask(1012, Priority.HIGH, - m(EvcsWebastoUnite.ChannelId.CURRENT_L3, new UnsignedWordElement(1012))), + m(ElectricityMeter.ChannelId.CURRENT_L3, new UnsignedWordElement(1012))), new FC4ReadInputRegistersTask(1014, Priority.LOW, - m(EvcsWebastoUnite.ChannelId.VOLTAGE_L1, new UnsignedWordElement(1014))), + m(ElectricityMeter.ChannelId.VOLTAGE_L1, new UnsignedWordElement(1014))), new FC4ReadInputRegistersTask(1016, Priority.LOW, - m(EvcsWebastoUnite.ChannelId.VOLTAGE_L2, new UnsignedWordElement(1016))), + m(ElectricityMeter.ChannelId.VOLTAGE_L2, new UnsignedWordElement(1016))), new FC4ReadInputRegistersTask(1018, Priority.LOW, - m(EvcsWebastoUnite.ChannelId.VOLTAGE_L3, new UnsignedWordElement(1018))), + m(ElectricityMeter.ChannelId.VOLTAGE_L3, new UnsignedWordElement(1018))), new FC4ReadInputRegistersTask(1020, Priority.HIGH, - m(EvcsWebastoUnite.ChannelId.ACTIVE_POWER_TOTAL, new UnsignedDoublewordElement(1020))), + m(ElectricityMeter.ChannelId.ACTIVE_POWER, new UnsignedDoublewordElement(1020))), new FC4ReadInputRegistersTask(1024, Priority.HIGH, - m(EvcsWebastoUnite.ChannelId.ACTIVE_POWER_L1, new UnsignedDoublewordElement(1024))), + m(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, new UnsignedDoublewordElement(1024))), new FC4ReadInputRegistersTask(1028, Priority.HIGH, - m(EvcsWebastoUnite.ChannelId.ACTIVE_POWER_L2, new UnsignedDoublewordElement(1028))), + m(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, new UnsignedDoublewordElement(1028))), new FC4ReadInputRegistersTask(1032, Priority.HIGH, - m(EvcsWebastoUnite.ChannelId.ACTIVE_POWER_L3, new UnsignedDoublewordElement(1032))), + m(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, new UnsignedDoublewordElement(1032))), new FC4ReadInputRegistersTask(1036, Priority.LOW, m(EvcsWebastoUnite.ChannelId.METER_READING, new UnsignedDoublewordElement(1036))), new FC4ReadInputRegistersTask(1100, Priority.LOW, @@ -207,6 +210,11 @@ protected ModbusProtocol defineModbusProtocol() { return modbusProtocol; } + @Override + public MeterType getMeterType() { + return MeterType.MANAGED_CONSUMPTION_METERED; + } + @Override public String debugLog() { return "Limit:" + this.getSetChargePowerLimit().orElse(null) + "|" + this.getStatus().getName(); diff --git a/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/WebastoReadHandler.java b/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/WebastoReadHandler.java index 3018446dacc..68a8e7fdbbb 100644 --- a/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/WebastoReadHandler.java +++ b/io.openems.edge.evcs.webasto.unite/src/io/openems/edge/evcs/webasto/unite/WebastoReadHandler.java @@ -1,5 +1,7 @@ package io.openems.edge.evcs.webasto.unite; +import static io.openems.edge.evcs.api.Evcs.evaluatePhaseCount; + import io.openems.edge.evcs.api.Status; // TODO: Can also be done by registering onSetNextValue listeners on the depending channel in the WebastoImpl. @@ -14,54 +16,28 @@ protected WebastoReadHandler(EvcsWebastoUniteImpl parent) { protected void run() { this.setPhaseCount(); this.setStatus(); - this.parent._setChargePower((int) this.parent.getActivePower()); } private void setStatus() { - switch (this.parent.getChargePointState()) { - case (0): - this.parent._setStatus(Status.NOT_READY_FOR_CHARGING); - break; - case (1): - this.parent._setStatus(Status.READY_FOR_CHARGING); - break; - case (2): - this.parent._setStatus(Status.CHARGING); - break; - case (3): - case (4): - this.parent._setStatus(Status.CHARGING_REJECTED); - break; - case (5): - // TODO Check if this state is also reached while paused - this.parent._setStatus(Status.CHARGING_FINISHED); - break; - case (7): - case (8): - this.parent._setStatus(Status.ERROR); - } + this.parent._setStatus(switch (this.parent.getChargePointState()) { + case 0 -> Status.NOT_READY_FOR_CHARGING; + case 1 -> Status.READY_FOR_CHARGING; + case 2 -> Status.CHARGING; + case 3, 4 -> Status.CHARGING_REJECTED; + case 5 -> Status.CHARGING_FINISHED; + // TODO Check if this state is also reached while paused + case 7, 8 -> Status.ERROR; + default -> null; + }); } /** * Writes the Amount of Phases in the Phase channel. */ private void setPhaseCount() { - int phases = 0; - /* - * The EVCS will pull power from the grid for its own consumption and report - * that on one of the phases. This value is different from EVCS to EVCS but can - * be high. Because of this, this will only register a Phase starting with 100W - * because then we definitively know that this load is caused by a car. - */ - if (this.parent.getActivePowerL1() >= 100) { - phases += 1; - } - if (this.parent.getActivePowerL2() >= 100) { - phases += 1; - } - if (this.parent.getActivePowerL3() >= 100) { - phases += 1; - } - this.parent._setPhases(phases); + this.parent._setPhases(evaluatePhaseCount(// + this.parent.getActivePowerL1().get(), // + this.parent.getActivePowerL2().get(), // + this.parent.getActivePowerL3().get())); } } diff --git a/io.openems.edge.evcs.webasto.unite/test/io/openems/edge/evcs/webasto/unite/EvcsWebastoUniteImplTest.java b/io.openems.edge.evcs.webasto.unite/test/io/openems/edge/evcs/webasto/unite/EvcsWebastoUniteImplTest.java index 41bc0851964..c5bea2c96cb 100644 --- a/io.openems.edge.evcs.webasto.unite/test/io/openems/edge/evcs/webasto/unite/EvcsWebastoUniteImplTest.java +++ b/io.openems.edge.evcs.webasto.unite/test/io/openems/edge/evcs/webasto/unite/EvcsWebastoUniteImplTest.java @@ -8,17 +8,14 @@ public class EvcsWebastoUniteImplTest { - private static final String EVCS_ID = "evcs0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new EvcsWebastoUniteImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setModbusId(MODBUS_ID) // - .setId(EVCS_ID) // + .setModbusId("modbus0") // + .setId("evcs0") // .setModbusUnitId(255) // .setMaxHwCurrent(32_000) // .setMinHwCurrent(6_000) // diff --git a/io.openems.edge.fenecon.dess/src/io/openems/edge/fenecon/dess/gridmeter/FeneconDessGridMeterImpl.java b/io.openems.edge.fenecon.dess/src/io/openems/edge/fenecon/dess/gridmeter/FeneconDessGridMeterImpl.java index 39580294e38..1c45cd1a254 100644 --- a/io.openems.edge.fenecon.dess/src/io/openems/edge/fenecon/dess/gridmeter/FeneconDessGridMeterImpl.java +++ b/io.openems.edge.fenecon.dess/src/io/openems/edge/fenecon/dess/gridmeter/FeneconDessGridMeterImpl.java @@ -18,6 +18,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; @@ -32,7 +33,6 @@ import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.fenecon.dess.FeneconDessConstants; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.fenecon.dess/src/io/openems/edge/fenecon/dess/pvmeter/FeneconDessPvMeterImpl.java b/io.openems.edge.fenecon.dess/src/io/openems/edge/fenecon/dess/pvmeter/FeneconDessPvMeterImpl.java index e9fc1e27ddf..f0e8b41de8d 100644 --- a/io.openems.edge.fenecon.dess/src/io/openems/edge/fenecon/dess/pvmeter/FeneconDessPvMeterImpl.java +++ b/io.openems.edge.fenecon.dess/src/io/openems/edge/fenecon/dess/pvmeter/FeneconDessPvMeterImpl.java @@ -16,6 +16,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; @@ -28,7 +29,6 @@ import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.fenecon.dess.FeneconDessConstants; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/charger/FeneconDessCharger1Test.java b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/charger/FeneconDessCharger1Test.java index 20325ec4cbf..146b7240c85 100644 --- a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/charger/FeneconDessCharger1Test.java +++ b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/charger/FeneconDessCharger1Test.java @@ -10,19 +10,15 @@ public class FeneconDessCharger1Test { - private static final String CHARGER_ID = "charger0"; - private static final String ESS_ID = "ess0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { var ess = new FeneconDessEssImpl(); new ComponentTest(ess) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyEssConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("ess0") // + .setModbusId("modbus0") // .build()) // ; @@ -30,9 +26,9 @@ public void test() throws Exception { .addReference("cm", new DummyConfigurationAdmin()) // .addReference("ess", ess) // .activate(MyChargerConfig.create() // - .setId(CHARGER_ID) // - .setEssId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("charger0") // + .setEssId("ess0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/charger/FeneconDessCharger2Test.java b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/charger/FeneconDessCharger2Test.java index 879abb0d8fe..18f69c54492 100644 --- a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/charger/FeneconDessCharger2Test.java +++ b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/charger/FeneconDessCharger2Test.java @@ -10,19 +10,15 @@ public class FeneconDessCharger2Test { - private static final String CHARGER_ID = "charger1"; - private static final String ESS_ID = "ess0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { var ess = new FeneconDessEssImpl(); new ComponentTest(ess) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyEssConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("ess0") // + .setModbusId("modbus0") // .build()) // ; @@ -30,9 +26,9 @@ public void test() throws Exception { .addReference("cm", new DummyConfigurationAdmin()) // .addReference("ess", ess) // .activate(MyChargerConfig.create() // - .setId(CHARGER_ID) // - .setEssId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("charger1") // + .setEssId("ess0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/ess/FeneconDessEssImplTest.java b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/ess/FeneconDessEssImplTest.java index 9a71817d9c8..8fb795dd74d 100644 --- a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/ess/FeneconDessEssImplTest.java +++ b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/ess/FeneconDessEssImplTest.java @@ -8,17 +8,14 @@ public class FeneconDessEssImplTest { - private static final String ESS_ID = "ess0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new FeneconDessEssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyEssConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("ess0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/gridmeter/FeneconDessGridMeterImplTest.java b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/gridmeter/FeneconDessGridMeterImplTest.java index fd85f499448..a29b0a99e7c 100644 --- a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/gridmeter/FeneconDessGridMeterImplTest.java +++ b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/gridmeter/FeneconDessGridMeterImplTest.java @@ -8,17 +8,14 @@ public class FeneconDessGridMeterImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new FeneconDessGridMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // + .setId("meter0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/pvmeter/FeneconDessPvMeterImplTest.java b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/pvmeter/FeneconDessPvMeterImplTest.java index c849a026396..4af1a5dfcd3 100644 --- a/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/pvmeter/FeneconDessPvMeterImplTest.java +++ b/io.openems.edge.fenecon.dess/test/io/openems/edge/fenecon/dess/pvmeter/FeneconDessPvMeterImplTest.java @@ -8,17 +8,14 @@ public class FeneconDessPvMeterImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new FeneconDessPvMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // + .setId("meter0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.fenecon.mini/src/io/openems/edge/fenecon/mini/gridmeter/FeneconMiniGridMeterImpl.java b/io.openems.edge.fenecon.mini/src/io/openems/edge/fenecon/mini/gridmeter/FeneconMiniGridMeterImpl.java index 9968e9c6e59..eeca54aa533 100644 --- a/io.openems.edge.fenecon.mini/src/io/openems/edge/fenecon/mini/gridmeter/FeneconMiniGridMeterImpl.java +++ b/io.openems.edge.fenecon.mini/src/io/openems/edge/fenecon/mini/gridmeter/FeneconMiniGridMeterImpl.java @@ -16,6 +16,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; @@ -29,7 +30,6 @@ import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.fenecon.mini.FeneconMiniConstants; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.fenecon.mini/src/io/openems/edge/fenecon/mini/pvmeter/FeneconMiniPvMeterImpl.java b/io.openems.edge.fenecon.mini/src/io/openems/edge/fenecon/mini/pvmeter/FeneconMiniPvMeterImpl.java index 009311b915e..39da4f147fe 100644 --- a/io.openems.edge.fenecon.mini/src/io/openems/edge/fenecon/mini/pvmeter/FeneconMiniPvMeterImpl.java +++ b/io.openems.edge.fenecon.mini/src/io/openems/edge/fenecon/mini/pvmeter/FeneconMiniPvMeterImpl.java @@ -16,6 +16,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -27,7 +28,6 @@ import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.fenecon.mini.FeneconMiniConstants; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/ess/FeneconMiniEssImplTest.java b/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/ess/FeneconMiniEssImplTest.java index 7535b25a0c3..b83208448ac 100644 --- a/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/ess/FeneconMiniEssImplTest.java +++ b/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/ess/FeneconMiniEssImplTest.java @@ -1,8 +1,11 @@ package io.openems.edge.fenecon.mini.ess; +import static io.openems.edge.fenecon.mini.ess.FeneconMiniEss.ChannelId.PCS_MODE; +import static io.openems.edge.fenecon.mini.ess.FeneconMiniEss.ChannelId.SETUP_MODE; +import static io.openems.edge.fenecon.mini.ess.FeneconMiniEss.ChannelId.STATE_MACHINE; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -13,14 +16,6 @@ public class FeneconMiniEssImplTest { - private static final String MODBUS_ID = "modbus0"; - - private static final String ESS_ID = "ess0"; - - private static final ChannelAddress ESS_STATE_MACHINE = new ChannelAddress(ESS_ID, "StateMachine"); - private static final ChannelAddress ESS_PCS_MODE = new ChannelAddress(ESS_ID, "PcsMode"); - private static final ChannelAddress ESS_SETUP_MODE = new ChannelAddress(ESS_ID, "SetupMode"); - /** * Tests activating write-mode when it was not activated before. * @@ -31,34 +26,34 @@ public void testWriteModeSet() throws Exception { new ManagedSymmetricEssTest(new FeneconMiniEssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("power", new DummyPower()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("ess0") // + .setModbusId("modbus0") // .setPhase(SinglePhase.L1) // .setReadonly(false) // .build()) // .next(new TestCase() // - .input(ESS_PCS_MODE, PcsMode.ECONOMIC) // - .input(ESS_SETUP_MODE, SetupMode.OFF) // - .output(ESS_STATE_MACHINE, State.UNDEFINED)) // + .input(PCS_MODE, PcsMode.ECONOMIC) // + .input(SETUP_MODE, SetupMode.OFF) // + .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.GO_WRITE_MODE)) // + .output(STATE_MACHINE, State.GO_WRITE_MODE)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.ACTIVATE_DEBUG_MODE_1)) // + .output(STATE_MACHINE, State.ACTIVATE_DEBUG_MODE_1)) // .next(new TestCase() // - .input(ESS_SETUP_MODE, SetupMode.ON) // - .output(ESS_STATE_MACHINE, State.ACTIVATE_DEBUG_MODE_2)) // + .input(SETUP_MODE, SetupMode.ON) // + .output(STATE_MACHINE, State.ACTIVATE_DEBUG_MODE_2)) // .next(new TestCase() // - .input(ESS_PCS_MODE, PcsMode.DEBUG) // - .output(ESS_STATE_MACHINE, State.ACTIVATE_DEBUG_MODE_3)) // + .input(PCS_MODE, PcsMode.DEBUG) // + .output(STATE_MACHINE, State.ACTIVATE_DEBUG_MODE_3)) // .next(new TestCase() // - .input(ESS_SETUP_MODE, SetupMode.OFF) // - .output(ESS_STATE_MACHINE, State.ACTIVATE_DEBUG_MODE_4)) // + .input(SETUP_MODE, SetupMode.OFF) // + .output(STATE_MACHINE, State.ACTIVATE_DEBUG_MODE_4)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.GO_WRITE_MODE)) // + .output(STATE_MACHINE, State.GO_WRITE_MODE)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.WRITE_MODE)) // + .output(STATE_MACHINE, State.WRITE_MODE)) // ; } @@ -72,21 +67,21 @@ public void testWriteModeAlreadySet() throws Exception { new ManagedSymmetricEssTest(new FeneconMiniEssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("power", new DummyPower()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("ess0") // + .setModbusId("modbus0") // .setPhase(SinglePhase.L1) // .setReadonly(false) // .build()) // .next(new TestCase() // - .input(ESS_PCS_MODE, PcsMode.DEBUG) // - .input(ESS_SETUP_MODE, SetupMode.OFF) // - .output(ESS_STATE_MACHINE, State.UNDEFINED)) // + .input(PCS_MODE, PcsMode.DEBUG) // + .input(SETUP_MODE, SetupMode.OFF) // + .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.GO_WRITE_MODE)) // + .output(STATE_MACHINE, State.GO_WRITE_MODE)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.WRITE_MODE)) // + .output(STATE_MACHINE, State.WRITE_MODE)) // ; } @@ -100,34 +95,34 @@ public void testReadonlyModeSet() throws Exception { new ManagedSymmetricEssTest(new FeneconMiniEssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("power", new DummyPower()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("ess0") // + .setModbusId("modbus0") // .setPhase(SinglePhase.L1) // .setReadonly(true) // .build()) // .next(new TestCase() // - .input(ESS_PCS_MODE, PcsMode.DEBUG) // - .input(ESS_SETUP_MODE, SetupMode.OFF) // - .output(ESS_STATE_MACHINE, State.UNDEFINED)) // + .input(PCS_MODE, PcsMode.DEBUG) // + .input(SETUP_MODE, SetupMode.OFF) // + .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.GO_READONLY_MODE)) // + .output(STATE_MACHINE, State.GO_READONLY_MODE)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_1)) // + .output(STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_1)) // .next(new TestCase() // - .input(ESS_SETUP_MODE, SetupMode.ON) // - .output(ESS_STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_2)) // + .input(SETUP_MODE, SetupMode.ON) // + .output(STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_2)) // .next(new TestCase() // - .input(ESS_PCS_MODE, PcsMode.ECONOMIC) // - .output(ESS_STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_3)) // + .input(PCS_MODE, PcsMode.ECONOMIC) // + .output(STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_3)) // .next(new TestCase() // - .input(ESS_SETUP_MODE, SetupMode.OFF) // - .output(ESS_STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_4)) // + .input(SETUP_MODE, SetupMode.OFF) // + .output(STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_4)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.GO_READONLY_MODE)) // + .output(STATE_MACHINE, State.GO_READONLY_MODE)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.READONLY_MODE)) // + .output(STATE_MACHINE, State.READONLY_MODE)) // ; } @@ -141,34 +136,34 @@ public void testReadonlyModeAlreadySet() throws Exception { new ManagedSymmetricEssTest(new FeneconMiniEssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("power", new DummyPower()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("ess0") // + .setModbusId("modbus0") // .setPhase(SinglePhase.L1) // .setReadonly(true) // .build()) // .next(new TestCase() // - .input(ESS_PCS_MODE, PcsMode.DEBUG) // - .input(ESS_SETUP_MODE, SetupMode.OFF) // - .output(ESS_STATE_MACHINE, State.UNDEFINED)) // + .input(PCS_MODE, PcsMode.DEBUG) // + .input(SETUP_MODE, SetupMode.OFF) // + .output(STATE_MACHINE, State.UNDEFINED)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.GO_READONLY_MODE)) // + .output(STATE_MACHINE, State.GO_READONLY_MODE)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_1)) // + .output(STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_1)) // .next(new TestCase() // - .input(ESS_SETUP_MODE, SetupMode.ON) // - .output(ESS_STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_2)) // + .input(SETUP_MODE, SetupMode.ON) // + .output(STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_2)) // .next(new TestCase() // - .input(ESS_PCS_MODE, PcsMode.ECONOMIC) // - .output(ESS_STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_3)) // + .input(PCS_MODE, PcsMode.ECONOMIC) // + .output(STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_3)) // .next(new TestCase() // - .input(ESS_SETUP_MODE, SetupMode.OFF) // - .output(ESS_STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_4)) // + .input(SETUP_MODE, SetupMode.OFF) // + .output(STATE_MACHINE, State.ACTIVATE_ECONOMIC_MODE_4)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.GO_READONLY_MODE)) // + .output(STATE_MACHINE, State.GO_READONLY_MODE)) // .next(new TestCase() // - .output(ESS_STATE_MACHINE, State.READONLY_MODE)) // + .output(STATE_MACHINE, State.READONLY_MODE)) // ; } } diff --git a/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/gridmeter/FeneconMiniGridMeterImplTest.java b/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/gridmeter/FeneconMiniGridMeterImplTest.java index 9f22123b8b5..e478c89f29f 100644 --- a/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/gridmeter/FeneconMiniGridMeterImplTest.java +++ b/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/gridmeter/FeneconMiniGridMeterImplTest.java @@ -8,17 +8,14 @@ public class FeneconMiniGridMeterImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new FeneconMiniGridMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // + .setId("meter0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/pvmeter/FeneconMiniPvMeterImplTest.java b/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/pvmeter/FeneconMiniPvMeterImplTest.java index 143b1e2d51a..59e45468495 100644 --- a/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/pvmeter/FeneconMiniPvMeterImplTest.java +++ b/io.openems.edge.fenecon.mini/test/io/openems/edge/fenecon/mini/pvmeter/FeneconMiniPvMeterImplTest.java @@ -8,17 +8,14 @@ public class FeneconMiniPvMeterImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new FeneconMiniPvMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // + .setId("meter0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.fenecon.pro/src/io/openems/edge/fenecon/pro/pvmeter/FeneconProPvMeterImpl.java b/io.openems.edge.fenecon.pro/src/io/openems/edge/fenecon/pro/pvmeter/FeneconProPvMeterImpl.java index 896686de264..3e40fd09bd7 100644 --- a/io.openems.edge.fenecon.pro/src/io/openems/edge/fenecon/pro/pvmeter/FeneconProPvMeterImpl.java +++ b/io.openems.edge.fenecon.pro/src/io/openems/edge/fenecon/pro/pvmeter/FeneconProPvMeterImpl.java @@ -17,6 +17,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -29,7 +30,6 @@ import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.fenecon.pro/test/io/openems/edge/fenecon/pro/ess/FeneconProEssImplTest.java b/io.openems.edge.fenecon.pro/test/io/openems/edge/fenecon/pro/ess/FeneconProEssImplTest.java index 05e9ee82109..31a40767aca 100644 --- a/io.openems.edge.fenecon.pro/test/io/openems/edge/fenecon/pro/ess/FeneconProEssImplTest.java +++ b/io.openems.edge.fenecon.pro/test/io/openems/edge/fenecon/pro/ess/FeneconProEssImplTest.java @@ -9,18 +9,15 @@ public class FeneconProEssImplTest { - private static final String ESS_ID = "ess0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new FeneconProEssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("power", new DummyPower()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("ess0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.fenecon.pro/test/io/openems/edge/fenecon/pro/pvmeter/FeneconProPvMeterImplTest.java b/io.openems.edge.fenecon.pro/test/io/openems/edge/fenecon/pro/pvmeter/FeneconProPvMeterImplTest.java index d9163f8438f..d46d42f0cc1 100644 --- a/io.openems.edge.fenecon.pro/test/io/openems/edge/fenecon/pro/pvmeter/FeneconProPvMeterImplTest.java +++ b/io.openems.edge.fenecon.pro/test/io/openems/edge/fenecon/pro/pvmeter/FeneconProPvMeterImplTest.java @@ -8,17 +8,14 @@ public class FeneconProPvMeterImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new FeneconProPvMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // + .setId("meter0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.goodwe/doc/GoodWeSerialNrRule.png b/io.openems.edge.goodwe/doc/GoodWeSerialNrRule.png new file mode 100644 index 00000000000..1e55c49d82c Binary files /dev/null and b/io.openems.edge.goodwe/doc/GoodWeSerialNrRule.png differ diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverter.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverter.java index fd50c862023..c8e725dca44 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverter.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverter.java @@ -16,7 +16,7 @@ public interface GoodWeBatteryInverter public static enum ChannelId implements io.openems.edge.common.channel.ChannelId { STATE_MACHINE(Doc.of(State.values()) // .text("Current State of State-Machine")), // - RUN_FAILED(Doc.of(Level.FAULT) // + RUN_FAILED(Doc.of(Level.WARNING) // .text("Running the Logic failed")); // private final Doc doc; diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java index 57f17030ddd..1b40ca0444b 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java @@ -115,7 +115,7 @@ protected void setModbus(BridgeModbus modbus) { protected static record BatteryData(Integer chargeMaxCurrent, Integer voltage) { } - private Config config; + private Config config = null; public GoodWeBatteryInverterImpl() throws OpenemsNamedException { super(// @@ -167,7 +167,7 @@ protected void deactivate() { @Override public void handleEvent(Event event) { - if (!this.isEnabled()) { + if (this.config == null || !this.isEnabled()) { return; } super.handleEvent(event); diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/AbstractGoodWeEtCharger.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/AbstractGoodWeEtCharger.java index cf6d669406e..97eb0bcba86 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/AbstractGoodWeEtCharger.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/AbstractGoodWeEtCharger.java @@ -88,17 +88,11 @@ private void updateState() { var goodWe = this.getEssOrBatteryInverter(); Boolean hasNoDcPv = null; if (goodWe != null) { - switch (goodWe.getGoodweType().getSeries()) { - case BT: - hasNoDcPv = true; - break; - case ET: - hasNoDcPv = false; - break; - case UNDEFINED: - hasNoDcPv = null; - break; - } + hasNoDcPv = switch (goodWe.getGoodweType().getSeries()) { + case BT -> true; + case ET, ETT -> false; + case UNDEFINED -> null; + }; } this._setHasNoDcPv(hasNoDcPv); } diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java index 3fcfec14317..e6765b39ca9 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java @@ -1195,10 +1195,12 @@ protected final ModbusProtocol defineModbusProtocol() { ); /* - * Handles different GoodWe Types. + * Handle different GoodWe Types. * - * Register 35011: GoodWeType as String (Not supported for GoodWe 20 & 30) - * Register 35003: Serial number as String (Fallback for GoodWe 20 & 30) + * GoodweType Firmware is differing from Type ET-Plus to ETT. + * + * Register 35011: GoodWeType as String (Not supported for GoodWe 20 & 30 - ETT) + * Register 35003: Serial number as String (Fallback for GoodWe 20 & 30 - ETT) */ readElementOnce(protocol, ModbusUtils::retryOnNull, new StringWordElement(35011, 5)) // .thenAccept(value -> { @@ -1210,8 +1212,30 @@ protected final ModbusProtocol defineModbusProtocol() { TypeUtils.getAsType(OpenemsType.STRING, value)); if (resultFromString != GoodWeType.UNDEFINED) { + + /* + * ET-Plus + */ this.logInfo(this.log, "Identified " + resultFromString.getName()); this._setGoodweType(resultFromString); + + // Handles different ET-Plus DSP versions + ModbusUtils.readElementOnce(protocol, ModbusUtils::retryOnNull, new UnsignedWordElement(35016)) // + .thenAccept(dspVersion -> { + try { + if (dspVersion >= 5) { + this.handleDspVersion5(protocol); + } + if (dspVersion >= 6) { + this.handleDspVersion6(protocol); + } + if (dspVersion >= 7) { + this.handleDspVersion7(protocol); + } + } catch (OpenemsException e) { + this.logError(this.log, "Unable to add task for modbus protocol"); + } + }); return; } @@ -1220,46 +1244,23 @@ protected final ModbusProtocol defineModbusProtocol() { */ readElementOnce(protocol, ModbusUtils::retryOnNull, new StringWordElement(35003, 8)) // .thenAccept(serialNr -> { + final var hardwareType = getGoodWeTypeFromSerialNr(serialNr); try { this._setGoodweType(hardwareType); + this.handleDspVersion5(protocol); + this.handleDspVersion6(protocol); + this.handleDspVersion7(protocol); if (hardwareType == GoodWeType.FENECON_FHI_20_DAH || hardwareType == GoodWeType.FENECON_FHI_29_9_DAH) { this.handleMultipleStringChargers(protocol); } - } catch (OpenemsException e) { this.logError(this.log, "Unable to add charger tasks for modbus protocol"); } }); }); - // Handles different DSP versions - readElementOnce(protocol, ModbusUtils::retryOnNull, new UnsignedWordElement(35016)) // - .thenAccept(dspVersion -> { - try { - - // GoodWe 30 has DspFmVersionMaster=0 & DspBetaVersion=80 - if (dspVersion == 0) { - this.handleDspVersion5(protocol); - this.handleDspVersion6(protocol); - this.handleDspVersion7(protocol); - return; - } - if (dspVersion >= 5) { - this.handleDspVersion5(protocol); - } - if (dspVersion >= 6) { - this.handleDspVersion6(protocol); - } - if (dspVersion >= 7) { - this.handleDspVersion7(protocol); - } - } catch (OpenemsException e) { - this.logError(this.log, "Unable to add task for modbus protocol"); - } - }); - return protocol; } @@ -1329,7 +1330,7 @@ protected static GoodWeType getGoodWeTypeFromSerialNr(String serialNr) { /** * Handle multiple string chargers. - * + * *

    * For MPPT connectors e.g. two string on one MPPT the power information is * spread over several registers that should be read as complete blocks. diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java index aa090eaa2b3..8fd557e1ffc 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java @@ -254,12 +254,12 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId .unit(Unit.KILOWATT_HOURS)), // // Error Message 35189 - STATE_0(Doc.of(Level.FAULT) // + STATE_0(Doc.of(Level.WARNING) // .text("The Ground Fault Circuit Interrupter (GFCI) detecting circuit is abnormal " // + "| Interne Fehlerstrom-Schutzeinrichtung (RCD Einheit) wurde ausgelöst " // + "| Bitte überprüfen Sie den Netzanschluss sowie ggf. Backup-Lasten")), // - STATE_1(Doc.of(Level.FAULT) // + STATE_1(Doc.of(Level.WARNING) // .text("The output current sensor is abnormal " // + "| Der Ausgangs-Stromsensor liefert unplausible Werte " // + "| Bitte überprüfen Sie die Installation")), // @@ -269,12 +269,12 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId STATE_2(Doc.of(Level.WARNING) // .text("Warning Code 1")), // - STATE_3(Doc.of(Level.FAULT) // + STATE_3(Doc.of(Level.WARNING) // .text("DCI Consistency Failure " // + "| Werte der Impedanzmessung (DCI Einheit) sind widersprüchlich/unplausibel " // + "| Bitte überprüfen Sie den Netzanschluss")), // - STATE_4(Doc.of(Level.FAULT) // + STATE_4(Doc.of(Level.WARNING) // .text("Ground Fault Circuit Interrupter (GFCI) Consistency Failure " // + "| Werte der internen Fehlerstrom-Schutzeinrichtung (RCD) sind widersprüchlich/unplausibel " // + "| Bitte überprüfen Sie den Netzanschluss")), // @@ -284,29 +284,29 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId STATE_5(Doc.of(Level.WARNING) // .text("Warning Code 2")), // - STATE_6(Doc.of(Level.FAULT) // + STATE_6(Doc.of(Level.WARNING) // .text("Ground Fault Circuit Interrupter (GFCI) Device Failure " // + "| Interne Fehlerstrom-Schutzeinrichtung (RCD Einheit) befindet sich im Fehlerzustand " // + "| Bitte führen Sie einen Geräteneustart aus")), // - STATE_7(Doc.of(Level.FAULT) // + STATE_7(Doc.of(Level.WARNING) // .text("Relay Device Failure " // + "| Interne Relais befinden sich im Fehlerzustand " // + "| Bitte führen Sie einen Geräteneustart aus")), // - STATE_8(Doc.of(Level.FAULT) // + STATE_8(Doc.of(Level.WARNING) // .text("AC HCT Failure " // + "| Die HCT Einheit befindet sich im Fehlerzustand " // + "| Bitte führen Sie einen Geräteneustart aus")), // - STATE_9(Doc.of(Level.FAULT) // + STATE_9(Doc.of(Level.WARNING) // .text("Utility Loss " // + "| Netzausfall wurde erkannt " // + "| Bitte überprüfen Sie ob das Kommunikationsmodul richtig gesteckt ist")), // // TODO: Use new-lines or html-lists when the UI and edge log are able to handle // them - STATE_10(Doc.of(Level.FAULT) // + STATE_10(Doc.of(Level.WARNING) // .text("Ground I Failure " // + "| Erdungsfehler " // + "| Ggf. N und PE Leiter sind nicht richtig mit dem Netzanschluss des Wechselrichters verbunden. " // @@ -320,7 +320,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId + "| Ggf. übersteigt die Leerlauf- oder Betriebsspannung der PV-Module den für diesen Wechselrichter zulässigen Bereich. " // + "Ggf. liegt ein PV-Kriechstrom zur Erde an")), // - STATE_12(Doc.of(Level.FAULT) // + STATE_12(Doc.of(Level.WARNING) // .text("Internal Fan Failure " // + "| Der interne Lüfter meldet einen Defekt")), // @@ -331,13 +331,13 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId + "Ggf. Luftstrom durch den Kühlkörper für Normalbetrieb unzureichend (Aufstellbedingungen beachten!). " + "Ggf. Behinderung des Luftstroms, z.B. Kühlkörper wurde abgedeckt")), // - STATE_14(Doc.of(Level.FAULT) // + STATE_14(Doc.of(Level.WARNING) // .text("Utility Phase Failure " // + "| Phasenfehler " // + "| Überprüfen Sie das Drehfeld am Wechselrichter. " // + "Ggf. Kommunikationsadapter (ET+) nicht (richtig) gesteckt")), // - STATE_15(Doc.of(Level.FAULT) // + STATE_15(Doc.of(Level.WARNING) // .text("PV Over Voltage " // + "| Überspannung PV " // + "| Bitte überprüfen Sie die Installation")), // @@ -346,13 +346,13 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId .text("External Fan Failure " // + "| Externer Lüfter befindet sich im Fehlerzustand")), // - STATE_17(Doc.of(Level.FAULT) // + STATE_17(Doc.of(Level.WARNING) // .text("Vac Failure " // + "| Spannungsfehler " // + "| Die anliegende Spannung am \"On-Grid\" Anschluss befindet sich außerhalb der gültigen Parameter (für DE siehe VDE AR N 4105). " // + "Ggf. Kommunikationsmodul nicht (richtig) gesteckt")), // - STATE_18(Doc.of(Level.FAULT) // + STATE_18(Doc.of(Level.WARNING) // .text("Isolation resistance of PV-plant too low " // + "| Isolationsfehler auf PV-Strings " // + "| Bitte überprüfen Sie die Installation")), // @@ -362,7 +362,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId + "| DC-Strom Einspeisung auf \"On-Grid\" Seite ist zu hoch " // + "| Bitte überprüfen Sie die Installation und angeschlossene Verbraucher bzw. Erzeuger")), // - STATE_20(Doc.of(Level.FAULT) // + STATE_20(Doc.of(Level.WARNING) // .text("Back-Up Over Load " // + "| Überlastung Backup-Anschluss " // + "| Bitte beachten Sie die im Datenblatt angegebenen Maximal-Lasten")), // @@ -372,12 +372,12 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId STATE_21(Doc.of(Level.WARNING) // .text("Warning Code 3")), // - STATE_22(Doc.of(Level.FAULT) // + STATE_22(Doc.of(Level.WARNING) // .text("Difference between Master and Slave frequency too high " // + "| Frequenz zwischen Master und Slave weicht zu stark ab " // + "| Bitte führen Sie einen Geräteneustart aus")), // - STATE_23(Doc.of(Level.FAULT) // + STATE_23(Doc.of(Level.WARNING) // .text("Difference between Master and Slave voltage too high " // + "| Spannung zwischen Master und Slave weicht zu stark ab " // + "| Bitte führen Sie einen Geräteneustart aus")), // @@ -387,7 +387,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId STATE_24(Doc.of(Level.WARNING) // .text("Warning Code 4")), // - STATE_25(Doc.of(Level.FAULT) // + STATE_25(Doc.of(Level.WARNING) // .text("Relay Check Failure " // + "| Selbsttest der Relais ist Fehlgeschlagen " // + "| Ggf. sind N und PE-Leiter nicht richtig mit den Anschlussklemmen des Wechselrichters verbunden. " // @@ -409,17 +409,17 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId + "| Kommunikation zwischen der ARM und DSP Einheit ist fehlgeschlagen " // + "| Bitte führen Sie einen Geräteneustart aus")), // - STATE_29(Doc.of(Level.FAULT) // + STATE_29(Doc.of(Level.WARNING) // .text("The grid frequency is out of tolerable range " // + "| Die Netz-Frequenz befindet sich außerhalb der zulässigen Parameter " // + "| Bitte überprüfen Sie die Installation und führen anschließend einen Geräteneustart aus")), // - STATE_30(Doc.of(Level.FAULT) // + STATE_30(Doc.of(Level.WARNING) // .text("EEPROM cannot be read or written " // + "| EEPROM kann nicht gelesen oder geschrieben werden " // + "| Bitte führen Sie einen Geräteneustart aus")), // - STATE_31(Doc.of(Level.FAULT) // + STATE_31(Doc.of(Level.WARNING) // .text("Communication failure between microcontrollers " // + "| Die Kommunikation zwischen den einzelnen Microkontrollern ist fehlerhaft " // + "| Bitte führen Sie einen Geräteneustart aus")), // @@ -1642,7 +1642,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId .text("SMART mode does not work correctly with active PID filter")), NO_SMART_METER_DETECTED(Doc.of(Level.WARNING) // .text("No GoodWe Smart Meter detected. Only REMOTE mode can work correctly")), - IMPOSSIBLE_FENECON_HOME_COMBINATION(Doc.of(Level.FAULT) // + IMPOSSIBLE_FENECON_HOME_COMBINATION(Doc.of(Level.WARNING) // .text("The installed inverter and battery combination is not authorised. Operation could cause hardware damages, so charging and discharging is blocked. Please install a complete Home 10, Home 20 or Home 30 system.")), // IGNORE_IMPOSSIBLE_P_BATTERY_VALUE(Doc.of(OpenemsType.BOOLEAN) // .text("Ignore impossible battery power")) // diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/enums/GoodWeType.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/enums/GoodWeType.java index 32e2a1cec1a..c927165fb09 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/enums/GoodWeType.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/enums/GoodWeType.java @@ -15,19 +15,17 @@ public enum GoodWeType implements OptionsEnum { GOODWE_10K_ET(20, "GoodWe GW10K-ET", Series.ET, 25), // GOODWE_8K_ET(21, "GoodWe GW8K-ET", Series.ET, 25), // GOODWE_5K_ET(22, "GoodWe GW5K-ET", Series.ET, 25), // - FENECON_FHI_10_DAH(30, "FENECON FHI 10 DAH", Series.ET, 25, position2Filter("10"), + FENECON_FHI_10_DAH(30, "FENECON FHI 10 DAH", Series.ET, 25, serialNrFilter("010K", "ETU"), (batteryType) -> batteryType != BatteryFeneconHomeHardwareType.BATTERY_52), // - FENECON_FHI_20_DAH(120, "FENECON FHI 20 DAH", Series.ET, 50, position2Filter("20"), + FENECON_FHI_20_DAH(120, "FENECON FHI 20 DAH", Series.ETT, 50, serialNrFilter("020K", "ETT"), (batteryType) -> batteryType != BatteryFeneconHomeHardwareType.BATTERY_64), // - FENECON_FHI_29_9_DAH(130, "FENECON FHI 30 DAH", Series.ET, 50, home30Filter("29K9", "30"), + FENECON_FHI_29_9_DAH(130, "FENECON FHI 30 DAH", Series.ETT, 50, home30Filter("29K9", "030K"), (batteryType) -> batteryType != BatteryFeneconHomeHardwareType.BATTERY_64); // public static enum Series { - UNDEFINED, BT, ET; + UNDEFINED, BT, ET, ETT; } - // TODO: Change logic of isValidHomeBattery to invalidBattery - private final int value; private final String option; private final Series series; @@ -100,8 +98,28 @@ public static ThrowingFunction position2Filter(Strin */ public static ThrowingFunction home30Filter(String... match) { return serialNr -> Stream.of(match) // - .filter(t -> t.equals(serialNr.substring(2, 4)) || serialNr.substring(1, 5).contains(t)) // + .filter(t -> { + try { + return serialNrFilter(t, "ETT").apply(serialNr); + } catch (Exception e) { + return false; + } + }) // .findFirst() // .isPresent(); } + + /** + * GoodWe serial number filter. + * + *

    + * Check if a serialNr matches a given string at the common position. + * + * @param ratedPower rated power of the inverter + * @param seriesCode internal inverter model series code + * @return filter function + */ + public static ThrowingFunction serialNrFilter(String ratedPower, String seriesCode) { + return serialNr -> serialNr.substring(1, 5).equals(ratedPower) && serialNr.substring(5, 8).equals(seriesCode); + } } \ No newline at end of file diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterImpl.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterImpl.java index a9643334fc1..de98f4d10be 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterImpl.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterImpl.java @@ -20,6 +20,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -35,7 +36,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImpl.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImpl.java index 3cb9157b12e..6e9a4fac228 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImpl.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImpl.java @@ -1,11 +1,14 @@ package io.openems.edge.goodwe.gridmeter; +import static io.openems.common.types.OpenemsType.INTEGER; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.INVERT; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_1; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_2; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_MINUS_2; import static io.openems.edge.bridge.modbus.api.ModbusUtils.readElementOnce; +import java.util.function.Supplier; + import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -26,7 +29,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; -import io.openems.common.types.OpenemsType; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; @@ -40,6 +43,7 @@ import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask; import io.openems.edge.bridge.modbus.api.task.FC6WriteRegisterTask; import io.openems.edge.common.channel.ChannelUtils; +import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.modbusslave.ModbusSlave; @@ -49,7 +53,6 @@ import io.openems.edge.common.type.TypeUtils; import io.openems.edge.ess.power.api.Phase; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; @@ -127,7 +130,6 @@ protected void deactivate() { @Override protected ModbusProtocol defineModbusProtocol() { var protocol = new ModbusProtocol(this, // - // States new FC3ReadRegistersTask(36003, Priority.LOW, m(new UnsignedWordElement(36003)).build().onUpdateCallback((value) -> { @@ -136,7 +138,7 @@ protected ModbusProtocol defineModbusProtocol() { m(GoodWeGridMeter.ChannelId.HAS_NO_METER, new UnsignedWordElement(36004), new ElementToChannelConverter(value -> { - Integer intValue = TypeUtils.getAsType(OpenemsType.INTEGER, value); + Integer intValue = TypeUtils.getAsType(INTEGER, value); if (intValue != null) { switch (intValue) { case 0: @@ -206,11 +208,14 @@ private void handleDspVersion4(ModbusProtocol protocol) { m(ElectricityMeter.ChannelId.VOLTAGE_L3, new UnsignedWordElement(36054), this.ignoreZeroAndScaleFactor2), // m(ElectricityMeter.ChannelId.CURRENT_L1, new UnsignedWordElement(36055), - this.ignoreZeroAndScaleFactor2), // + ElementToChannelConverter.chain(this.ignoreZeroAndScaleFactor2, // + createAdjustCurrentSign(this.getActivePowerL1Channel()::getNextValue))), // m(ElectricityMeter.ChannelId.CURRENT_L2, new UnsignedWordElement(36056), - this.ignoreZeroAndScaleFactor2), // + ElementToChannelConverter.chain(this.ignoreZeroAndScaleFactor2, // + createAdjustCurrentSign(this.getActivePowerL2Channel()::getNextValue))), // m(ElectricityMeter.ChannelId.CURRENT_L3, new UnsignedWordElement(36057), - this.ignoreZeroAndScaleFactor2))); // + ElementToChannelConverter.chain(this.ignoreZeroAndScaleFactor2, // + createAdjustCurrentSign(this.getActivePowerL3Channel()::getNextValue))))); // } private void handleExternalMeter(ModbusProtocol protocol) { @@ -318,7 +323,7 @@ protected void convertMeterConnectStatus(Integer value) { /** * Get the connection value depending on the phase. - * + * *

    * The information of each phase connection is part of a hex. The part of the * given phase will be returned. @@ -406,4 +411,24 @@ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { ModbusSlaveNatureTable.of(GoodWeGridMeter.class, accessMode, 100).build() // ); } + + /** + * Creates an {@link ElementToChannelConverter} for + * {@link ElectricityMeter.ChannelId#CURRENT_L1}, + * {@link ElectricityMeter.ChannelId#CURRENT_L2} and + * {@link ElectricityMeter.ChannelId#CURRENT_L3} that adjusts the sign to that + * given by a supplier. + * + * @param getActivePowerNextValue {@link Supplier} for a value with a sign that + * should be copied + * @return the {@link ElementToChannelConverter} + */ + protected static ElementToChannelConverter createAdjustCurrentSign( + Supplier> getActivePowerNextValue) { + return new ElementToChannelConverter(value -> { + var activePower = getActivePowerNextValue.get().orElse(0); + Integer intValue = TypeUtils.getAsType(INTEGER, value); + return Math.abs(intValue) * Integer.signum(activePower); + }); + } } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImplTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImplTest.java index 5473b431528..c8180b66657 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImplTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImplTest.java @@ -1,13 +1,45 @@ package io.openems.edge.goodwe.batteryinverter; +import static io.openems.edge.battery.api.Battery.ChannelId.CHARGE_MAX_CURRENT; +import static io.openems.edge.batteryinverter.api.SymmetricBatteryInverter.ChannelId.ACTIVE_POWER; +import static io.openems.edge.batteryinverter.api.SymmetricBatteryInverter.ChannelId.MAX_APPARENT_POWER; +import static io.openems.edge.common.sum.Sum.ChannelId.GRID_ACTIVE_POWER; +import static io.openems.edge.ess.dccharger.api.EssDcCharger.ChannelId.ACTUAL_POWER; +import static io.openems.edge.ess.dccharger.api.EssDcCharger.ChannelId.CURRENT; +import static io.openems.edge.ess.dccharger.api.EssDcCharger.ChannelId.VOLTAGE; +import static io.openems.edge.goodwe.GoodWeConstants.DEFAULT_UNIT_ID; import static io.openems.edge.goodwe.batteryinverter.GoodWeBatteryInverterImpl.doSetBmsVoltage; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.EMS_POWER_MODE; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.EMS_POWER_SET; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.MAX_AC_EXPORT; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.MAX_AC_IMPORT; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.METER_COMMUNICATE_STATUS; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.MPPT1_I; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.MPPT1_P; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.MPPT2_I; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.MPPT2_P; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.MPPT3_I; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.MPPT3_P; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV1_I; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV1_V; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV2_I; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV2_V; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV3_I; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV3_V; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV4_I; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV4_V; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV5_I; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV5_V; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV6_I; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.TWO_S_PV6_V; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.WBMS_CHARGE_MAX_CURRENT; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.WBMS_DISCHARGE_MAX_CURRENT; +import static io.openems.edge.goodwe.common.GoodWe.ChannelId.WBMS_VOLTAGE; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; -import io.openems.common.types.ChannelAddress; -import io.openems.edge.battery.api.Battery; import io.openems.edge.battery.test.DummyBattery; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.channel.value.Value; @@ -18,7 +50,6 @@ import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.ess.test.DummyPower; -import io.openems.edge.goodwe.GoodWeConstants; import io.openems.edge.goodwe.charger.singlestring.GoodWeChargerPv1; import io.openems.edge.goodwe.charger.twostring.GoodWeChargerTwoStringImpl; import io.openems.edge.goodwe.charger.twostring.PvPort; @@ -32,85 +63,17 @@ @SuppressWarnings("deprecation") public class GoodWeBatteryInverterImplTest { - private static final String MODBUS_ID = "modbus0"; - private static final String BATTERY_ID = "battery0"; - private static final String BATTERY_INVERTER_ID = "batteryInverter0"; - private static final String CHARGER_ID = "charger0"; - private static final String CHARGER_2_ID = "charger1"; - private static final String CHARGER_3_ID = "charger2"; - private static final String CHARGER_4_ID = "charger3"; - private static final String CHARGER_5_ID = "charger4"; - private static final String CHARGER_6_ID = "charger5"; - private static final String SUM_ID = "_sum"; - - private static final Battery BATTERY = new DummyBattery(BATTERY_ID); - - private static final ChannelAddress EMS_POWER_MODE = new ChannelAddress(BATTERY_INVERTER_ID, "EmsPowerMode"); - private static final ChannelAddress EMS_POWER_SET = new ChannelAddress(BATTERY_INVERTER_ID, "EmsPowerSet"); - private static final ChannelAddress METER_COMMUNICATE_STATUS = new ChannelAddress(BATTERY_INVERTER_ID, - "MeterCommunicateStatus"); - private static final ChannelAddress MAX_AC_IMPORT = new ChannelAddress(BATTERY_INVERTER_ID, "MaxAcImport"); - private static final ChannelAddress MAX_AC_EXPORT = new ChannelAddress(BATTERY_INVERTER_ID, "MaxAcExport"); - private static final ChannelAddress GRID_ACTIVE_POWER = new ChannelAddress(SUM_ID, "GridActivePower"); - private static final ChannelAddress ACTIVE_POWER = new ChannelAddress(BATTERY_INVERTER_ID, "ActivePower"); - private static final ChannelAddress CHARGE_MAX_CURRENT = new ChannelAddress(BATTERY_ID, "ChargeMaxCurrent"); - private static final ChannelAddress WBMS_CHARGE_MAX_CURRENT = new ChannelAddress(BATTERY_INVERTER_ID, - "WbmsChargeMaxCurrent"); - private static final ChannelAddress WBMS_DISCHARGE_MAX_CURRENT = new ChannelAddress(BATTERY_INVERTER_ID, - "WbmsDischargeMaxCurrent"); - private static final ChannelAddress WBMS_VOLTAGE = new ChannelAddress(BATTERY_INVERTER_ID, "WbmsVoltage"); - private static final ChannelAddress MAX_APPARENT_POWER = new ChannelAddress(BATTERY_INVERTER_ID, - "MaxApparentPower"); - - private static final ChannelAddress CHARGER_ACTUAL_POWER = new ChannelAddress(CHARGER_ID, "ActualPower"); - private static final ChannelAddress CHARGER_VOLTAGE = new ChannelAddress(CHARGER_ID, "Voltage"); - private static final ChannelAddress CHARGER_CURRENT = new ChannelAddress(CHARGER_ID, "Current"); - private static final ChannelAddress CHARGER_2_ACTUAL_POWER = new ChannelAddress(CHARGER_2_ID, "ActualPower"); - private static final ChannelAddress CHARGER_2_VOLTAGE = new ChannelAddress(CHARGER_2_ID, "Voltage"); - private static final ChannelAddress CHARGER_2_CURRENT = new ChannelAddress(CHARGER_2_ID, "Current"); - private static final ChannelAddress CHARGER_3_ACTUAL_POWER = new ChannelAddress(CHARGER_3_ID, "ActualPower"); - private static final ChannelAddress CHARGER_3_VOLTAGE = new ChannelAddress(CHARGER_3_ID, "Voltage"); - private static final ChannelAddress CHARGER_3_CURRENT = new ChannelAddress(CHARGER_3_ID, "Current"); - private static final ChannelAddress CHARGER_4_ACTUAL_POWER = new ChannelAddress(CHARGER_4_ID, "ActualPower"); - private static final ChannelAddress CHARGER_4_VOLTAGE = new ChannelAddress(CHARGER_4_ID, "Voltage"); - private static final ChannelAddress CHARGER_4_CURRENT = new ChannelAddress(CHARGER_4_ID, "Current"); - private static final ChannelAddress CHARGER_5_ACTUAL_POWER = new ChannelAddress(CHARGER_5_ID, "ActualPower"); - private static final ChannelAddress CHARGER_5_VOLTAGE = new ChannelAddress(CHARGER_5_ID, "Voltage"); - private static final ChannelAddress CHARGER_5_CURRENT = new ChannelAddress(CHARGER_5_ID, "Current"); - private static final ChannelAddress CHARGER_6_ACTUAL_POWER = new ChannelAddress(CHARGER_6_ID, "ActualPower"); - private static final ChannelAddress CHARGER_6_VOLTAGE = new ChannelAddress(CHARGER_6_ID, "Voltage"); - private static final ChannelAddress CHARGER_6_CURRENT = new ChannelAddress(CHARGER_6_ID, "Current"); - - private static final ChannelAddress MPPT1_P = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt1P"); - private static final ChannelAddress MPPT1_I = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt1I"); - private static final ChannelAddress MPPT2_P = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt2P"); - private static final ChannelAddress MPPT2_I = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt2I"); - private static final ChannelAddress MPPT3_P = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt3P"); - private static final ChannelAddress MPPT3_I = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt3I"); - private static final ChannelAddress TWO_S_PV1_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv1I"); - private static final ChannelAddress TWO_S_PV1_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv1V"); - private static final ChannelAddress TWO_S_PV2_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv2I"); - private static final ChannelAddress TWO_S_PV2_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv2V"); - private static final ChannelAddress TWO_S_PV3_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv3I"); - private static final ChannelAddress TWO_S_PV3_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv3V"); - private static final ChannelAddress TWO_S_PV4_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv4I"); - private static final ChannelAddress TWO_S_PV4_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv4V"); - private static final ChannelAddress TWO_S_PV5_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv5I"); - private static final ChannelAddress TWO_S_PV5_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv5V"); - private static final ChannelAddress TWO_S_PV6_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv6I"); - private static final ChannelAddress TWO_S_PV6_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv6V"); - @Test public void testEt() throws Exception { var charger = new GoodWeChargerPv1(); new ComponentTest(charger) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(io.openems.edge.goodwe.charger.singlestring.MyConfig.create() // - .setId(CHARGER_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("charger0") // + .setBatteryInverterId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .build()); var ess = new GoodWeBatteryInverterImpl(); @@ -119,13 +82,13 @@ public void testEt() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .addComponent(charger) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -140,9 +103,9 @@ public void testEt() throws Exception { .input(ACTIVE_POWER, 0) // .input(MAX_AC_IMPORT, 0) // .input(MAX_AC_EXPORT, 0) // - .input(CHARGER_ACTUAL_POWER, 2000) // + .input("charger0", ACTUAL_POWER, 2000) // .onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, 1000, 0); + ess.run(new DummyBattery("battery0"), 1000, 0); }) // .output(EMS_POWER_MODE, EmsPowerMode.CHARGE_BAT) // .output(EMS_POWER_SET, 1000)); @@ -155,12 +118,12 @@ public void testNegativSetActivePoint() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -176,7 +139,7 @@ public void testNegativSetActivePoint() throws Exception { .input(MAX_AC_IMPORT, 0) // .input(MAX_AC_EXPORT, 0) // .onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, -1000, 0); + ess.run(new DummyBattery("battery0"), -1000, 0); }) // .output(EMS_POWER_MODE, EmsPowerMode.CHARGE_BAT) // .output(EMS_POWER_SET, 1000)); @@ -189,12 +152,12 @@ public void testDischargeBattery() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -210,7 +173,7 @@ public void testDischargeBattery() throws Exception { .input(MAX_AC_IMPORT, 0) // .input(MAX_AC_EXPORT, 0) // .onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, 1000, 0); + ess.run(new DummyBattery("battery0"), 1000, 0); }) // .output(EMS_POWER_MODE, EmsPowerMode.DISCHARGE_BAT) // .output(EMS_POWER_SET, 1000)); @@ -223,12 +186,12 @@ public void testEmsPowerModeAutoWithBalancing() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -243,7 +206,7 @@ public void testEmsPowerModeAutoWithBalancing() throws Exception { .input(GRID_ACTIVE_POWER, 2000) // .input(ACTIVE_POWER, 4000) // .onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, 6000, 0); + ess.run(new DummyBattery("battery0"), 6000, 0); }) // .output(EMS_POWER_MODE, EmsPowerMode.AUTO) // .output(EMS_POWER_SET, 0)); @@ -254,12 +217,12 @@ public void testEmsPowerModeAutoWithSurplus() throws Exception { var charger = new GoodWeChargerPv1(); new ComponentTest(charger) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(io.openems.edge.goodwe.charger.singlestring.MyConfig.create() // - .setId(CHARGER_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("charger0") // + .setBatteryInverterId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .build()); var ess = new GoodWeBatteryInverterImpl(); @@ -267,15 +230,15 @@ public void testEmsPowerModeAutoWithSurplus() throws Exception { new ComponentTest(ess) // .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("componentManager", new DummyComponentManager()) // .addReference("sum", new DummySum()) // .addComponent(charger) // - .addComponent(BATTERY) // + .addComponent(new DummyBattery("battery0")) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -287,9 +250,9 @@ public void testEmsPowerModeAutoWithSurplus() throws Exception { .build()) // .next(new TestCase() // .input(METER_COMMUNICATE_STATUS, MeterCommunicateStatus.OK) // - .input(CHARGER_ACTUAL_POWER, 10000) // - .input(CHARGE_MAX_CURRENT, 20).onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, 10000, 0); + .input("charger0", ACTUAL_POWER, 10000) // + .input("battery0", CHARGE_MAX_CURRENT, 20).onExecuteWriteCallbacks(() -> { + ess.run(new DummyBattery("battery0"), 10000, 0); }) // .output(EMS_POWER_MODE, EmsPowerMode.AUTO) // .output(EMS_POWER_SET, 0)); @@ -302,12 +265,12 @@ public void testEmsPowerModeAutoWithMaxAcImport() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -321,7 +284,7 @@ public void testEmsPowerModeAutoWithMaxAcImport() throws Exception { .input(METER_COMMUNICATE_STATUS, MeterCommunicateStatus.OK) // .input(MAX_AC_IMPORT, 3000) // .onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, 3000, 0); + ess.run(new DummyBattery("battery0"), 3000, 0); }) // .output(EMS_POWER_MODE, EmsPowerMode.AUTO) // .output(EMS_POWER_SET, 0)); @@ -334,12 +297,12 @@ public void testEmsPowerModeAutoWithMaxAcExport() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -353,7 +316,7 @@ public void testEmsPowerModeAutoWithMaxAcExport() throws Exception { .input(METER_COMMUNICATE_STATUS, MeterCommunicateStatus.OK) // .input(MAX_AC_EXPORT, 8000) // .onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, 8000, 0); + ess.run(new DummyBattery("battery0"), 8000, 0); }) // .output(EMS_POWER_MODE, EmsPowerMode.AUTO) // .output(EMS_POWER_SET, 0)); @@ -366,12 +329,12 @@ public void testBatteryIsFull() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -385,7 +348,7 @@ public void testBatteryIsFull() throws Exception { .input(METER_COMMUNICATE_STATUS, MeterCommunicateStatus.OK) // .input(MAX_AC_IMPORT, 0) // .onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, 0, 0); + ess.run(new DummyBattery("battery0"), 0, 0); }) // .output(EMS_POWER_MODE, EmsPowerMode.AUTO) // .output(EMS_POWER_SET, 0)); @@ -398,12 +361,12 @@ public void testBatteryIsEmpty() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -417,7 +380,7 @@ public void testBatteryIsEmpty() throws Exception { .input(METER_COMMUNICATE_STATUS, MeterCommunicateStatus.OK) // .input(MAX_AC_EXPORT, 0) // .onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, 0, 0); + ess.run(new DummyBattery("battery0"), 0, 0); }) // .output(EMS_POWER_MODE, EmsPowerMode.AUTO) // .output(EMS_POWER_SET, 0)); @@ -430,12 +393,12 @@ public void testAcCalculation() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // - .addComponent(BATTERY).activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .addComponent(new DummyBattery("battery0")).activate(MyConfig.create() // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -451,7 +414,7 @@ public void testAcCalculation() throws Exception { .input(WBMS_VOLTAGE, 325) // .input(MAX_APPARENT_POWER, 10000) // .onExecuteWriteCallbacks(() -> { - ess.run(BATTERY, 0, 0); + ess.run(new DummyBattery("battery0"), 0, 0); }) // .output(MAX_AC_IMPORT, 0) // .output(MAX_AC_EXPORT, 325)); @@ -471,8 +434,8 @@ public void testTwoStringCharger() throws Exception { .addReference("cm", new DummyConfigurationAdmin()) // .addReference("essOrBatteryInverter", ess) // .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // - .setId(CHARGER_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setId("charger0") // + .setBatteryInverterId("batteryInverter0") // .setPvPort(PvPort.PV_1) // .build()); @@ -480,8 +443,8 @@ public void testTwoStringCharger() throws Exception { .addReference("cm", new DummyConfigurationAdmin()) // .addReference("essOrBatteryInverter", ess) // .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // - .setId(CHARGER_2_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setId("charger1") // + .setBatteryInverterId("batteryInverter0") // .setPvPort(PvPort.PV_2) // .build()); @@ -489,8 +452,8 @@ public void testTwoStringCharger() throws Exception { .addReference("cm", new DummyConfigurationAdmin()) // .addReference("essOrBatteryInverter", ess) // .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // - .setId(CHARGER_3_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setId("charger2") // + .setBatteryInverterId("batteryInverter0") // .setPvPort(PvPort.PV_3) // .build()); @@ -498,8 +461,8 @@ public void testTwoStringCharger() throws Exception { .addReference("cm", new DummyConfigurationAdmin()) // .addReference("essOrBatteryInverter", ess) // .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // - .setId(CHARGER_4_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setId("charger3") // + .setBatteryInverterId("batteryInverter0") // .setPvPort(PvPort.PV_4) // .build()); @@ -507,8 +470,8 @@ public void testTwoStringCharger() throws Exception { .addReference("cm", new DummyConfigurationAdmin()) // .addReference("essOrBatteryInverter", ess) // .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // - .setId(CHARGER_5_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setId("charger4") // + .setBatteryInverterId("batteryInverter0") // .setPvPort(PvPort.PV_5) // .build()); @@ -516,8 +479,8 @@ public void testTwoStringCharger() throws Exception { .addReference("cm", new DummyConfigurationAdmin()) // .addReference("essOrBatteryInverter", ess) // .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // - .setId(CHARGER_6_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setId("charger5") // + .setBatteryInverterId("batteryInverter0") // .setPvPort(PvPort.PV_6) // .build()); @@ -527,15 +490,15 @@ public void testTwoStringCharger() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .addComponent(charger1) // .addComponent(charger2) // - .addComponent(BATTERY) // + .addComponent(new DummyBattery("battery0")) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -554,19 +517,19 @@ public void testTwoStringCharger() throws Exception { .input(TWO_S_PV2_V, 240) // // Values applied in the next cycle - .output(CHARGER_ACTUAL_POWER, 0) // - .output(CHARGER_2_ACTUAL_POWER, 0) // - .output(CHARGER_CURRENT, null) // - .output(CHARGER_2_CURRENT, null) // - .output(CHARGER_VOLTAGE, null) // - .output(CHARGER_2_VOLTAGE, null)) // + .output("charger0", ACTUAL_POWER, 0) // + .output("charger1", ACTUAL_POWER, 0) // + .output("charger0", CURRENT, null) // + .output("charger1", CURRENT, null) // + .output("charger0", VOLTAGE, null) // + .output("charger1", VOLTAGE, null)) // .next(new TestCase() // - .output(CHARGER_ACTUAL_POWER, 1000) // - .output(CHARGER_2_ACTUAL_POWER, 1000) // - .output(CHARGER_CURRENT, 10) // - .output(CHARGER_2_CURRENT, 10) // - .output(CHARGER_VOLTAGE, 240) // - .output(CHARGER_2_VOLTAGE, 240)) // + .output("charger0", ACTUAL_POWER, 1000) // + .output("charger1", ACTUAL_POWER, 1000) // + .output("charger0", CURRENT, 10) // + .output("charger1", CURRENT, 10) // + .output("charger0", VOLTAGE, 240) // + .output("charger1", VOLTAGE, 240)) // // Chargers with different current values .next(new TestCase() // @@ -574,22 +537,22 @@ public void testTwoStringCharger() throws Exception { .input(MPPT1_P, 2000) // .input(TWO_S_PV1_I, 5) // .input(TWO_S_PV2_I, 15) // - .output(CHARGER_ACTUAL_POWER, 1000) // - .output(CHARGER_2_ACTUAL_POWER, 1000)) // + .output("charger0", ACTUAL_POWER, 1000) // + .output("charger1", ACTUAL_POWER, 1000)) // .next(new TestCase() // - .output(CHARGER_ACTUAL_POWER, 500) // - .output(CHARGER_2_ACTUAL_POWER, 1500)) // + .output("charger0", ACTUAL_POWER, 500) // + .output("charger1", ACTUAL_POWER, 1500)) // .next(new TestCase() // .input(MPPT1_I, 20) // .input(MPPT1_P, 2000) // .input(TWO_S_PV1_I, 20) // .input(TWO_S_PV2_I, 0) // - .output(CHARGER_ACTUAL_POWER, 500) // - .output(CHARGER_2_ACTUAL_POWER, 1500)) // + .output("charger0", ACTUAL_POWER, 500) // + .output("charger1", ACTUAL_POWER, 1500)) // .next(new TestCase() // - .output(CHARGER_ACTUAL_POWER, 2000) // - .output(CHARGER_2_ACTUAL_POWER, 0) // + .output("charger0", ACTUAL_POWER, 2000) // + .output("charger1", ACTUAL_POWER, 0) // ); /* @@ -601,15 +564,15 @@ public void testTwoStringCharger() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .addComponent(charger3) // .addComponent(charger4) // - .addComponent(BATTERY) // + .addComponent(new DummyBattery("battery0")) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -628,19 +591,19 @@ public void testTwoStringCharger() throws Exception { .input(TWO_S_PV4_V, 240) // // Values applied in the next cycle - .output(CHARGER_3_ACTUAL_POWER, 0) // - .output(CHARGER_4_ACTUAL_POWER, 0) // - .output(CHARGER_3_CURRENT, null) // - .output(CHARGER_4_CURRENT, null) // - .output(CHARGER_3_VOLTAGE, null) // - .output(CHARGER_4_VOLTAGE, null)) // + .output("charger2", ACTUAL_POWER, 0) // + .output("charger3", ACTUAL_POWER, 0) // + .output("charger2", CURRENT, null) // + .output("charger3", CURRENT, null) // + .output("charger2", VOLTAGE, null) // + .output("charger3", VOLTAGE, null)) // .next(new TestCase() // - .output(CHARGER_3_ACTUAL_POWER, 1000) // - .output(CHARGER_4_ACTUAL_POWER, 1000) // - .output(CHARGER_3_CURRENT, 10) // - .output(CHARGER_4_CURRENT, 10) // - .output(CHARGER_3_VOLTAGE, 240) // - .output(CHARGER_4_VOLTAGE, 240)) // + .output("charger2", ACTUAL_POWER, 1000) // + .output("charger3", ACTUAL_POWER, 1000) // + .output("charger2", CURRENT, 10) // + .output("charger3", CURRENT, 10) // + .output("charger2", VOLTAGE, 240) // + .output("charger3", VOLTAGE, 240)) // // Chargers with different current values .next(new TestCase() // @@ -648,22 +611,22 @@ public void testTwoStringCharger() throws Exception { .input(MPPT2_P, 2000) // .input(TWO_S_PV3_I, 5) // .input(TWO_S_PV4_I, 15) // - .output(CHARGER_3_ACTUAL_POWER, 1000) // - .output(CHARGER_4_ACTUAL_POWER, 1000)) // + .output("charger2", ACTUAL_POWER, 1000) // + .output("charger3", ACTUAL_POWER, 1000)) // .next(new TestCase() // - .output(CHARGER_3_ACTUAL_POWER, 500) // - .output(CHARGER_4_ACTUAL_POWER, 1500)) // + .output("charger2", ACTUAL_POWER, 500) // + .output("charger3", ACTUAL_POWER, 1500)) // .next(new TestCase() // .input(MPPT2_I, 20) // .input(MPPT2_P, 2000) // .input(TWO_S_PV3_I, 20) // .input(TWO_S_PV4_I, 0) // - .output(CHARGER_3_ACTUAL_POWER, 500) // - .output(CHARGER_4_ACTUAL_POWER, 1500)) // + .output("charger2", ACTUAL_POWER, 500) // + .output("charger3", ACTUAL_POWER, 1500)) // .next(new TestCase() // - .output(CHARGER_3_ACTUAL_POWER, 2000) // - .output(CHARGER_4_ACTUAL_POWER, 0) // + .output("charger2", ACTUAL_POWER, 2000) // + .output("charger3", ACTUAL_POWER, 0) // ); /* @@ -675,15 +638,15 @@ public void testTwoStringCharger() throws Exception { .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .addComponent(charger5) // .addComponent(charger6) // - .addComponent(BATTERY) // + .addComponent(new DummyBattery("battery0")) // .activate(MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // .setBackupEnable(EnableDisable.ENABLE) // @@ -702,19 +665,19 @@ public void testTwoStringCharger() throws Exception { .input(TWO_S_PV6_V, 240) // // Values applied in the next cycle - .output(CHARGER_5_ACTUAL_POWER, 0) // - .output(CHARGER_6_ACTUAL_POWER, 0) // - .output(CHARGER_5_CURRENT, null) // - .output(CHARGER_6_CURRENT, null) // - .output(CHARGER_5_VOLTAGE, null) // - .output(CHARGER_6_VOLTAGE, null)) // + .output("charger4", ACTUAL_POWER, 0) // + .output("charger5", ACTUAL_POWER, 0) // + .output("charger4", CURRENT, null) // + .output("charger5", CURRENT, null) // + .output("charger4", VOLTAGE, null) // + .output("charger5", VOLTAGE, null)) // .next(new TestCase() // - .output(CHARGER_5_ACTUAL_POWER, 1000) // - .output(CHARGER_6_ACTUAL_POWER, 1000) // - .output(CHARGER_5_CURRENT, 10) // - .output(CHARGER_6_CURRENT, 10) // - .output(CHARGER_5_VOLTAGE, 240) // - .output(CHARGER_6_VOLTAGE, 240)) // + .output("charger4", ACTUAL_POWER, 1000) // + .output("charger5", ACTUAL_POWER, 1000) // + .output("charger4", CURRENT, 10) // + .output("charger5", CURRENT, 10) // + .output("charger4", VOLTAGE, 240) // + .output("charger5", VOLTAGE, 240)) // // Chargers with different current values .next(new TestCase() // @@ -722,22 +685,22 @@ public void testTwoStringCharger() throws Exception { .input(MPPT3_P, 2000) // .input(TWO_S_PV5_I, 5) // .input(TWO_S_PV6_I, 15) // - .output(CHARGER_5_ACTUAL_POWER, 1000) // - .output(CHARGER_6_ACTUAL_POWER, 1000)) // + .output("charger4", ACTUAL_POWER, 1000) // + .output("charger5", ACTUAL_POWER, 1000)) // .next(new TestCase() // - .output(CHARGER_5_ACTUAL_POWER, 500) // - .output(CHARGER_6_ACTUAL_POWER, 1500)) // + .output("charger4", ACTUAL_POWER, 500) // + .output("charger5", ACTUAL_POWER, 1500)) // .next(new TestCase() // .input(MPPT3_I, 20) // .input(MPPT3_P, 2000) // .input(TWO_S_PV5_I, 20) // .input(TWO_S_PV6_I, 0) // - .output(CHARGER_5_ACTUAL_POWER, 500) // - .output(CHARGER_6_ACTUAL_POWER, 1500)) // + .output("charger4", ACTUAL_POWER, 500) // + .output("charger5", ACTUAL_POWER, 1500)) // .next(new TestCase() // - .output(CHARGER_5_ACTUAL_POWER, 2000) // - .output(CHARGER_6_ACTUAL_POWER, 0) // + .output("charger4", ACTUAL_POWER, 2000) // + .output("charger5", ACTUAL_POWER, 0) // ); } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/mppt/twostring/GoodWeChargerMpptTwoStringImplTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/mppt/twostring/GoodWeChargerMpptTwoStringImplTest.java index 591db516cd4..a7ad15707c2 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/mppt/twostring/GoodWeChargerMpptTwoStringImplTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/mppt/twostring/GoodWeChargerMpptTwoStringImplTest.java @@ -2,8 +2,6 @@ import org.junit.Test; -import io.openems.common.types.ChannelAddress; -import io.openems.edge.battery.api.Battery; import io.openems.edge.battery.test.DummyBattery; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.startstop.StartStopConfig; @@ -12,9 +10,11 @@ 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.ess.dccharger.api.EssDcCharger; import io.openems.edge.ess.test.DummyPower; import io.openems.edge.goodwe.GoodWeConstants; import io.openems.edge.goodwe.batteryinverter.GoodWeBatteryInverterImpl; +import io.openems.edge.goodwe.common.GoodWe; import io.openems.edge.goodwe.common.enums.ControlMode; import io.openems.edge.goodwe.common.enums.EnableDisable; import io.openems.edge.goodwe.common.enums.FeedInPowerSettings; @@ -22,94 +22,57 @@ public class GoodWeChargerMpptTwoStringImplTest { - private static final String MODBUS_ID = "modbus0"; - private static final String BATTERY_ID = "battery0"; - private static final String CHARGER_1_ID = "charger0"; - private static final String CHARGER_2_ID = "charger1"; - private static final String CHARGER_3_ID = "charger2"; - private static final String BATTERY_INVERTER_ID = "batteryInverter0"; - - private static final Battery BATTERY = new DummyBattery(BATTERY_ID); - - private static final ChannelAddress MPPT1_P = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt1P"); - private static final ChannelAddress MPPT1_I = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt1I"); - private static final ChannelAddress MPPT2_P = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt2P"); - private static final ChannelAddress MPPT2_I = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt2I"); - private static final ChannelAddress MPPT3_P = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt3P"); - private static final ChannelAddress MPPT3_I = new ChannelAddress(BATTERY_INVERTER_ID, "Mppt3I"); - private static final ChannelAddress TWO_S_PV1_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv1I"); - private static final ChannelAddress TWO_S_PV1_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv1V"); - private static final ChannelAddress TWO_S_PV2_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv2I"); - private static final ChannelAddress TWO_S_PV2_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv2V"); - private static final ChannelAddress TWO_S_PV3_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv3I"); - private static final ChannelAddress TWO_S_PV3_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv3V"); - private static final ChannelAddress TWO_S_PV4_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv4I"); - private static final ChannelAddress TWO_S_PV4_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv4V"); - private static final ChannelAddress TWO_S_PV5_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv5I"); - private static final ChannelAddress TWO_S_PV5_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv5V"); - private static final ChannelAddress TWO_S_PV6_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv6I"); - private static final ChannelAddress TWO_S_PV6_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv6V"); - private static final ChannelAddress CHARGER_1_ACTUAL_POWER = new ChannelAddress(CHARGER_1_ID, "ActualPower"); - private static final ChannelAddress CHARGER_1_VOLTAGE = new ChannelAddress(CHARGER_1_ID, "Voltage"); - private static final ChannelAddress CHARGER_1_CURRENT = new ChannelAddress(CHARGER_1_ID, "Current"); - private static final ChannelAddress CHARGER_2_ACTUAL_POWER = new ChannelAddress(CHARGER_2_ID, "ActualPower"); - private static final ChannelAddress CHARGER_2_VOLTAGE = new ChannelAddress(CHARGER_2_ID, "Voltage"); - private static final ChannelAddress CHARGER_2_CURRENT = new ChannelAddress(CHARGER_2_ID, "Current"); - private static final ChannelAddress CHARGER_3_ACTUAL_POWER = new ChannelAddress(CHARGER_3_ID, "ActualPower"); - private static final ChannelAddress CHARGER_3_VOLTAGE = new ChannelAddress(CHARGER_3_ID, "Voltage"); - private static final ChannelAddress CHARGER_3_CURRENT = new ChannelAddress(CHARGER_3_ID, "Current"); - @Test public void test() throws Exception { - var ess = new GoodWeBatteryInverterImpl(); + var inverter = new GoodWeBatteryInverterImpl(); var charger1 = new GoodWeChargerMpptTwoStringImpl(); var charger2 = new GoodWeChargerMpptTwoStringImpl(); var charger3 = new GoodWeChargerMpptTwoStringImpl(); new ComponentTest(charger1) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("essOrBatteryInverter", ess) // + .addReference("essOrBatteryInverter", inverter) // .activate(MyConfig.create() // - .setId(CHARGER_1_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setId("charger0") // + .setBatteryInverterId("batteryInverter0") // .setMpptPort(MpptPort.MPPT_1) // .build()); new ComponentTest(charger2) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("essOrBatteryInverter", ess) // + .addReference("essOrBatteryInverter", inverter) // .activate(MyConfig.create() // - .setId(CHARGER_2_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setId("charger1") // + .setBatteryInverterId("batteryInverter0") // .setMpptPort(MpptPort.MPPT_2) // .build()); new ComponentTest(charger3) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("essOrBatteryInverter", ess) // + .addReference("essOrBatteryInverter", inverter) // .activate(MyConfig.create() // - .setId(CHARGER_3_ID) // - .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setId("charger2") // + .setBatteryInverterId("batteryInverter0") // .setMpptPort(MpptPort.MPPT_3) // .build()); - ess.addCharger(charger1); - ess.addCharger(charger2); - ess.addCharger(charger3); + inverter.addCharger(charger1); + inverter.addCharger(charger2); + inverter.addCharger(charger3); - new ComponentTest(ess) // + new ComponentTest(inverter) // .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("componentManager", new DummyComponentManager()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addReference("sum", new DummySum()) // .addComponent(charger1) // .addComponent(charger2) // .addComponent(charger3) // - .addComponent(BATTERY) // + .addComponent(new DummyBattery("battery0")) // .activate(io.openems.edge.goodwe.batteryinverter.MyConfig.create() // - .setId(BATTERY_INVERTER_ID) // - .setModbusId(MODBUS_ID) // + .setId("batteryInverter0") // + .setModbusId("modbus0") // .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // .setSafetyCountry(SafetyCountry.GERMANY) // .setMpptForShadowEnable(EnableDisable.ENABLE) // @@ -121,74 +84,74 @@ public void test() throws Exception { .setStartStop(StartStopConfig.START) // .build()) // .next(new TestCase() // - .input(MPPT1_I, 20) // - .input(MPPT1_P, 2000) // - .input(TWO_S_PV1_I, 10) // - .input(TWO_S_PV2_I, 10) // - .input(TWO_S_PV1_V, 240) // - .input(TWO_S_PV2_V, 240)) // + .input(GoodWe.ChannelId.MPPT1_I, 20) // + .input(GoodWe.ChannelId.MPPT1_P, 2000) // + .input(GoodWe.ChannelId.TWO_S_PV1_I, 10) // + .input(GoodWe.ChannelId.TWO_S_PV2_I, 10) // + .input(GoodWe.ChannelId.TWO_S_PV1_V, 240) // + .input(GoodWe.ChannelId.TWO_S_PV2_V, 240)) // // Values applied in the next cycle .next(new TestCase() // - .output(CHARGER_1_ACTUAL_POWER, 2000) // - .output(CHARGER_1_CURRENT, 20) // - .output(CHARGER_1_VOLTAGE, 240) // - .output(CHARGER_2_ACTUAL_POWER, null) // - .output(CHARGER_2_CURRENT, null) // - .output(CHARGER_2_VOLTAGE, null) // - .output(CHARGER_3_ACTUAL_POWER, null) // - .output(CHARGER_3_CURRENT, null) // - .output(CHARGER_3_VOLTAGE, null) // + .output("charger0", EssDcCharger.ChannelId.ACTUAL_POWER, 2000) // + .output("charger0", EssDcCharger.ChannelId.CURRENT, 20) // + .output("charger0", EssDcCharger.ChannelId.VOLTAGE, 240) // + .output("charger1", EssDcCharger.ChannelId.ACTUAL_POWER, null) // + .output("charger1", EssDcCharger.ChannelId.CURRENT, null) // + .output("charger1", EssDcCharger.ChannelId.VOLTAGE, null) // + .output("charger2", EssDcCharger.ChannelId.ACTUAL_POWER, null) // + .output("charger2", EssDcCharger.ChannelId.CURRENT, null) // + .output("charger2", EssDcCharger.ChannelId.VOLTAGE, null) // ) // // Chargers with different current values .next(new TestCase() // - .input(MPPT1_I, 20) // - .input(MPPT1_P, 3000) // - .input(TWO_S_PV1_I, 5) // - .input(TWO_S_PV2_I, 15) // - .input(TWO_S_PV1_V, 250) // - .input(TWO_S_PV2_V, 250)) // + .input(GoodWe.ChannelId.MPPT1_I, 20) // + .input(GoodWe.ChannelId.MPPT1_P, 3000) // + .input(GoodWe.ChannelId.TWO_S_PV1_I, 5) // + .input(GoodWe.ChannelId.TWO_S_PV2_I, 15) // + .input(GoodWe.ChannelId.TWO_S_PV1_V, 250) // + .input(GoodWe.ChannelId.TWO_S_PV2_V, 250)) // .next(new TestCase() // - .output(CHARGER_1_ACTUAL_POWER, 3000) // - .output(CHARGER_1_CURRENT, 20) // - .output(CHARGER_1_VOLTAGE, 250) // - .output(CHARGER_2_ACTUAL_POWER, null) // - .output(CHARGER_2_CURRENT, null) // - .output(CHARGER_2_VOLTAGE, null). // - output(CHARGER_3_ACTUAL_POWER, null) // - .output(CHARGER_3_CURRENT, null) // - .output(CHARGER_3_VOLTAGE, null) // + .output("charger0", EssDcCharger.ChannelId.ACTUAL_POWER, 3000) // + .output("charger0", EssDcCharger.ChannelId.CURRENT, 20) // + .output("charger0", EssDcCharger.ChannelId.VOLTAGE, 250) // + .output("charger1", EssDcCharger.ChannelId.ACTUAL_POWER, null) // + .output("charger1", EssDcCharger.ChannelId.CURRENT, null) // + .output("charger1", EssDcCharger.ChannelId.VOLTAGE, null) // + .output("charger2", EssDcCharger.ChannelId.ACTUAL_POWER, null) // + .output("charger2", EssDcCharger.ChannelId.CURRENT, null) // + .output("charger2", EssDcCharger.ChannelId.VOLTAGE, null) // ) .next(new TestCase() // - .input(MPPT1_I, 20) // - .input(MPPT1_P, 2000) // - .input(MPPT2_I, 30) // - .input(MPPT2_P, 3000) // - .input(MPPT3_I, 40) // - .input(MPPT3_P, 4000) // - .input(TWO_S_PV1_I, 10) // - .input(TWO_S_PV1_V, 250) // - .input(TWO_S_PV2_I, 10) // - .input(TWO_S_PV2_V, 250) // - .input(TWO_S_PV3_I, 15) // - .input(TWO_S_PV3_V, 280) // - .input(TWO_S_PV4_I, 15) // - .input(TWO_S_PV4_V, 280) // - .input(TWO_S_PV5_I, 20) // - .input(TWO_S_PV5_V, 299) // - .input(TWO_S_PV6_I, 20) // - .input(TWO_S_PV6_V, 299)) // + .input(GoodWe.ChannelId.MPPT1_I, 20) // + .input(GoodWe.ChannelId.MPPT1_P, 2000) // + .input(GoodWe.ChannelId.MPPT2_I, 30) // + .input(GoodWe.ChannelId.MPPT2_P, 3000) // + .input(GoodWe.ChannelId.MPPT3_I, 40) // + .input(GoodWe.ChannelId.MPPT3_P, 4000) // + .input(GoodWe.ChannelId.TWO_S_PV1_I, 10) // + .input(GoodWe.ChannelId.TWO_S_PV1_V, 250) // + .input(GoodWe.ChannelId.TWO_S_PV2_I, 10) // + .input(GoodWe.ChannelId.TWO_S_PV2_V, 250) // + .input(GoodWe.ChannelId.TWO_S_PV3_I, 15) // + .input(GoodWe.ChannelId.TWO_S_PV3_V, 280) // + .input(GoodWe.ChannelId.TWO_S_PV4_I, 15) // + .input(GoodWe.ChannelId.TWO_S_PV4_V, 280) // + .input(GoodWe.ChannelId.TWO_S_PV5_I, 20) // + .input(GoodWe.ChannelId.TWO_S_PV5_V, 299) // + .input(GoodWe.ChannelId.TWO_S_PV6_I, 20) // + .input(GoodWe.ChannelId.TWO_S_PV6_V, 299)) // .next(new TestCase() // - .output(CHARGER_1_ACTUAL_POWER, 2000) // - .output(CHARGER_1_CURRENT, 20) // - .output(CHARGER_1_VOLTAGE, 250) // - .output(CHARGER_2_ACTUAL_POWER, 3000) // - .output(CHARGER_2_CURRENT, 30) // - .output(CHARGER_2_VOLTAGE, 280). // - output(CHARGER_3_ACTUAL_POWER, 4000) // - .output(CHARGER_3_CURRENT, 40) // - .output(CHARGER_3_VOLTAGE, 299) // + .output("charger0", EssDcCharger.ChannelId.ACTUAL_POWER, 2000) // + .output("charger0", EssDcCharger.ChannelId.CURRENT, 20) // + .output("charger0", EssDcCharger.ChannelId.VOLTAGE, 250) // + .output("charger1", EssDcCharger.ChannelId.ACTUAL_POWER, 3000) // + .output("charger1", EssDcCharger.ChannelId.CURRENT, 30) // + .output("charger1", EssDcCharger.ChannelId.VOLTAGE, 280) // + .output("charger2", EssDcCharger.ChannelId.ACTUAL_POWER, 4000) // + .output("charger2", EssDcCharger.ChannelId.CURRENT, 40) // + .output("charger2", EssDcCharger.ChannelId.VOLTAGE, 299) // ); } } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/singlestring/GoodWeChargerPv1Test.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/singlestring/GoodWeChargerPv1Test.java index 36ec9b90b80..e296380ac59 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/singlestring/GoodWeChargerPv1Test.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/singlestring/GoodWeChargerPv1Test.java @@ -8,19 +8,15 @@ public class GoodWeChargerPv1Test { - private static final String MODBUS_ID = "modbus0"; - private static final String ESS_ID = "ess0"; - private static final String CHARGER_ID = "charger0"; - @Test public void test() throws Exception { new ComponentTest(new GoodWeChargerPv1()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(CHARGER_ID) // - .setBatteryInverterId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("charger0") // + .setBatteryInverterId("ess0") // + .setModbusId("modbus0") // .build()); } } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/singlestring/GoodWeChargerPv2Test.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/singlestring/GoodWeChargerPv2Test.java index 754badab30e..77dd4956860 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/singlestring/GoodWeChargerPv2Test.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/singlestring/GoodWeChargerPv2Test.java @@ -8,19 +8,15 @@ public class GoodWeChargerPv2Test { - private static final String MODBUS_ID = "modbus0"; - private static final String ESS_ID = "ess0"; - private static final String CHARGER_ID = "charger0"; - @Test public void test() throws Exception { new ComponentTest(new GoodWeChargerPv2()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(CHARGER_ID) // - .setBatteryInverterId(ESS_ID) // - .setModbusId(MODBUS_ID) // + .setId("charger0") // + .setBatteryInverterId("ess0") // + .setModbusId("modbus0") // .build()); } } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImplTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImplTest.java index 3d2e5340e78..2c678e67e38 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImplTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImplTest.java @@ -8,9 +8,6 @@ public class GoodWeChargerTwoStringImplTest { - private static final String ESS_ID = "ess0"; - private static final String CHARGER_ID = "charger0"; - @SuppressWarnings("deprecation") @Test public void test() throws Exception { @@ -18,8 +15,8 @@ public void test() throws Exception { .addReference("cm", new DummyConfigurationAdmin()) // .addReference("essOrBatteryInverter", new GoodWeEssImpl()) // .activate(MyConfig.create() // - .setId(CHARGER_ID) // - .setBatteryInverterId(ESS_ID) // + .setId("charger0") // + .setBatteryInverterId("ess0") // .setPvPort(PvPort.PV_1) // .build()); } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/common/TestStatic.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/common/TestStatic.java index 65fe2da9a22..2889a5135ff 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/common/TestStatic.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/common/TestStatic.java @@ -23,13 +23,12 @@ public void testGetHardwareTypeFromSerialNr() { assertNotEquals(GoodWeType.FENECON_FHI_20_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("9010KETT22AW0004")); assertEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("9030KETT228W0004")); + assertEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("129K9ETT231W0159")); assertNotEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("9020KETT228W0004")); - assertEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("929K9ETT231W0159")); assertNotEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("929KETT231W0159")); assertNotEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("928K9ETT231W0159")); - assertEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("929K9ETT231W0160")); - assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("9040KETT228W0004")); + assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("9036KETT228W0004")); assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("9000KETT228W0004")); assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("ET2")); assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("")); diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterTest.java index 6c08cecae9b..463fd4d32f4 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterTest.java @@ -8,18 +8,14 @@ public class GoodWeEmergencyPowerMeterTest { - private static final String MODBUS_ID = "modbus0"; - - private static final String METER_ID = "meter2"; - @Test public void test() throws Exception { new ComponentTest(new GoodWeEmergencyPowerMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // + .setId("meter2") // + .setModbusId("modbus0") // .build()); } } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/ess/GoodWeEssImplTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/ess/GoodWeEssImplTest.java index bfde5088729..f6dec937023 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/ess/GoodWeEssImplTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/ess/GoodWeEssImplTest.java @@ -1,5 +1,7 @@ package io.openems.edge.goodwe.ess; +import static io.openems.edge.goodwe.GoodWeConstants.DEFAULT_UNIT_ID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; @@ -7,27 +9,22 @@ import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.ess.test.DummyPower; import io.openems.edge.ess.test.ManagedSymmetricEssTest; -import io.openems.edge.goodwe.GoodWeConstants; import io.openems.edge.goodwe.charger.singlestring.GoodWeChargerPv1; import io.openems.edge.goodwe.common.enums.ControlMode; public class GoodWeEssImplTest { - private static final String ESS_ID = "ess0"; - private static final String MODBUS_ID = "modbus0"; - private static final String CHARGER_ID = "charger0"; - @Test public void testEt() throws Exception { var charger = new GoodWeChargerPv1(); new ComponentTest(charger) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(io.openems.edge.goodwe.charger.singlestring.MyConfig.create() // - .setId(CHARGER_ID) // - .setBatteryInverterId(ESS_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("charger0") // + .setBatteryInverterId("ess0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .build()); var ess = new GoodWeEssImpl(); @@ -35,12 +32,12 @@ public void testEt() throws Exception { new ManagedSymmetricEssTest(ess) // .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .addComponent(charger) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("ess0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setCapacity(9_000) // .setMaxBatteryPower(5_200) // .setControlMode(ControlMode.SMART) // @@ -54,11 +51,11 @@ public void testBt() throws Exception { new ManagedSymmetricEssTest(ess) // .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setModbusId(MODBUS_ID) // - .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setId("ess0") // + .setModbusId("modbus0") // + .setModbusUnitId(DEFAULT_UNIT_ID) // .setCapacity(9_000) // .setMaxBatteryPower(5_200) // .setControlMode(ControlMode.SMART) // diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImplTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImplTest.java index 47b532280ad..c32d8e8b7f8 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImplTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImplTest.java @@ -1,42 +1,38 @@ package io.openems.edge.goodwe.gridmeter; +import static io.openems.edge.goodwe.gridmeter.GoodWeGridMeter.ChannelId.EXTERNAL_METER_RATIO; +import static io.openems.edge.goodwe.gridmeter.GoodWeGridMeter.ChannelId.METER_CON_CORRECTLY_L1; +import static io.openems.edge.goodwe.gridmeter.GoodWeGridMeter.ChannelId.METER_CON_INCORRECTLY_L1; +import static io.openems.edge.goodwe.gridmeter.GoodWeGridMeter.ChannelId.METER_CON_REVERSE_L1; +import static io.openems.edge.goodwe.gridmeter.GoodWeGridMeterCategory.COMMERCIAL_METER; +import static io.openems.edge.goodwe.gridmeter.GoodWeGridMeterCategory.SMART_METER; +import static io.openems.edge.goodwe.gridmeter.GoodWeGridMeterImpl.calculateRatio; +import static io.openems.edge.goodwe.gridmeter.GoodWeGridMeterImpl.getPhaseConnectionValue; import static org.junit.Assert.assertEquals; import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; +import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.ess.power.api.Phase; +import io.openems.edge.meter.api.ElectricityMeter; public class GoodWeGridMeterImplTest { - private static final String MODBUS_ID = "modbus0"; - - private static final String METER_ID = "meter0"; - - private static final ChannelAddress METER_CON_CORRECTLY_L1 = new ChannelAddress(METER_ID, - GoodWeGridMeter.ChannelId.METER_CON_CORRECTLY_L1.id()); - private static final ChannelAddress METER_CON_INCORRECTLY_L1 = new ChannelAddress(METER_ID, - GoodWeGridMeter.ChannelId.METER_CON_INCORRECTLY_L1.id()); - private static final ChannelAddress METER_CON_REVERSE_L1 = new ChannelAddress(METER_ID, - GoodWeGridMeter.ChannelId.METER_CON_REVERSE_L1.id()); - private static final ChannelAddress EXTERNAL_METER_RATIO = new ChannelAddress(METER_ID, - GoodWeGridMeter.ChannelId.EXTERNAL_METER_RATIO.id()); - @Test public void test() throws Exception { final var sut = new GoodWeGridMeterImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setGoodWeMeterCategory(GoodWeGridMeterCategory.SMART_METER) // + .setId("meter0") // + .setModbusId("modbus0") // + .setGoodWeMeterCategory(SMART_METER) // .setExternalMeterRatioValueA(0) // .setExternalMeterRatioValueB(0) // .build()) // @@ -65,44 +61,104 @@ public void test() throws Exception { @Test public void testMeterConnectStateConverter() throws Exception { - var l1Result = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L1, 0x0124); - var l2Result = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L2, 0x0124); - var l3Result = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L3, 0x0124); + var l1Result = getPhaseConnectionValue(Phase.L1, 0x0124); + var l2Result = getPhaseConnectionValue(Phase.L2, 0x0124); + var l3Result = getPhaseConnectionValue(Phase.L3, 0x0124); assertEquals(4, (int) l1Result); assertEquals(2, (int) l2Result); assertEquals(1, (int) l3Result); - l1Result = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L1, 0x0524); - l2Result = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L2, 0x0462); - l3Result = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L3, 0x1647); + l1Result = getPhaseConnectionValue(Phase.L1, 0x0524); + l2Result = getPhaseConnectionValue(Phase.L2, 0x0462); + l3Result = getPhaseConnectionValue(Phase.L3, 0x1647); assertEquals(4, (int) l1Result); assertEquals(6, (int) l2Result); assertEquals(6, (int) l3Result); - var l1NoResult = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L1, 0x000); - var l2NoResult = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L2, 0x000); - var l3NoResult = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L3, 0x000); + var l1NoResult = getPhaseConnectionValue(Phase.L1, 0x000); + var l2NoResult = getPhaseConnectionValue(Phase.L2, 0x000); + var l3NoResult = getPhaseConnectionValue(Phase.L3, 0x000); assertEquals(0, (int) l1NoResult); assertEquals(0, (int) l2NoResult); assertEquals(0, (int) l3NoResult); - var noResult = GoodWeGridMeterImpl.getPhaseConnectionValue(Phase.L3, 0x000); + var noResult = getPhaseConnectionValue(Phase.L3, 0x000); assert noResult == 0x000; } + @Test + public void testReadFromModbus() throws Exception { + new ComponentTest(new GoodWeGridMeterImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("setModbus", new DummyModbusBridge("modbus0") // + .withRegisters(36003, 0, 1) // States + .withRegisters(35123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) // F_GRID_R etc. + .withRegister(35016, 4) // DSP Version + .withRegisters(36005, // + /* ACTIVE_POWER */ -1000 /* L1 */, -1320 /* L2 */, 1610 /* L3 */, // + /* reserved */ 0, 0, 0, 0, 0, // + /* METER_POWER_FACTOR */ 0, // + /* FREQUENCY */ 50) + .withRegisters(36052, // + /* VOLTAGE */ 2000 /* L1 */, 2200 /* L2 */, 2300 /* L3 */, // + /* CURRENT */ 50 /* L1 */, 60 /* L2 */, 70 /* L3 */)) + .activate(MyConfig.create() // + .setId("meter0") // + .setModbusId("modbus0") // + .setGoodWeMeterCategory(SMART_METER) // + .setExternalMeterRatioValueA(0) // + .setExternalMeterRatioValueB(0) // + .build()) // + + .next(new TestCase() // + .output(GoodWeGridMeter.ChannelId.HAS_NO_METER, false) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 1000) // inverted + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, 1320) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, -1610) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 710)) // + + .next(new TestCase(), 3) // Wait for 36052 + .next(new TestCase() // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 200_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 220_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 230_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 5_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 6_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, -7_000)) // inverted + .deactivate(); + } + + @Test + public void testAdjustCurrentSign() { + { + var e2cConverter = GoodWeGridMeterImpl.createAdjustCurrentSign(() -> new Value(null, -5000)); + // postive to negative + assertEquals(-16, e2cConverter.elementToChannel(16)); + // negative stays negative + assertEquals(-16, e2cConverter.elementToChannel(-16)); + } + { + var e2cConverter = GoodWeGridMeterImpl.createAdjustCurrentSign(() -> new Value(null, 5000)); + // positive stays positive + assertEquals(16, e2cConverter.elementToChannel(16)); + // negative to positive + assertEquals(16, e2cConverter.elementToChannel(-16)); + } + } + @Test public void testExternalMeterRatio() throws Exception { new ComponentTest(new GoodWeGridMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setGoodWeMeterCategory(GoodWeGridMeterCategory.COMMERCIAL_METER) // + .setId("meter0") // + .setModbusId("modbus0") // + .setGoodWeMeterCategory(COMMERCIAL_METER) // .setExternalMeterRatioValueA(3000) // .setExternalMeterRatioValueB(5) // .build()) // @@ -111,11 +167,11 @@ public void testExternalMeterRatio() throws Exception { new ComponentTest(new GoodWeGridMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setGoodWeMeterCategory(GoodWeGridMeterCategory.COMMERCIAL_METER) // + .setId("meter0") // + .setModbusId("modbus0") // + .setGoodWeMeterCategory(COMMERCIAL_METER) // .setExternalMeterRatioValueA(500) // .setExternalMeterRatioValueB(5) // .build()) // @@ -124,11 +180,11 @@ public void testExternalMeterRatio() throws Exception { new ComponentTest(new GoodWeGridMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setGoodWeMeterCategory(GoodWeGridMeterCategory.SMART_METER) // + .setId("meter0") // + .setModbusId("modbus0") // + .setGoodWeMeterCategory(SMART_METER) // .setExternalMeterRatioValueA(3000) // .setExternalMeterRatioValueB(5) // .build()) // @@ -138,12 +194,11 @@ public void testExternalMeterRatio() throws Exception { @Test public void testCalculateRatio() { - - assertEquals(600, (int) GoodWeGridMeterImpl.calculateRatio(3000, 5)); - assertEquals(100, (int) GoodWeGridMeterImpl.calculateRatio(500, 5)); - assertEquals(null, GoodWeGridMeterImpl.calculateRatio(-5, 5)); - assertEquals(null, GoodWeGridMeterImpl.calculateRatio(3000, 0)); - assertEquals(null, GoodWeGridMeterImpl.calculateRatio(500, -5)); + assertEquals(600, calculateRatio(3000, 5).intValue()); + assertEquals(100, calculateRatio(500, 5).intValue()); + assertEquals(null, calculateRatio(-5, 5)); + assertEquals(null, calculateRatio(3000, 0)); + assertEquals(null, calculateRatio(500, -5)); } } diff --git a/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyCustomInputOutput.java b/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyCustomInputOutput.java new file mode 100644 index 00000000000..694f61e0348 --- /dev/null +++ b/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyCustomInputOutput.java @@ -0,0 +1,57 @@ +package io.openems.edge.io.test; + +import io.openems.common.channel.AccessMode; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.BooleanReadChannel; +import io.openems.edge.common.channel.BooleanWriteChannel; +import io.openems.edge.common.channel.ChannelId.ChannelIdImpl; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.test.AbstractDummyOpenemsComponent; +import io.openems.edge.io.api.DigitalInput; +import io.openems.edge.io.api.DigitalOutput; + +/** + * Provides a simple, simulated Digital Input/Output component that can be used + * together with the OpenEMS Component test framework. + */ +public class DummyCustomInputOutput extends AbstractDummyOpenemsComponent + implements DigitalInput, DigitalOutput { + + private final BooleanWriteChannel[] ioChannels; + + public DummyCustomInputOutput(String id) { + this(id, "INPUT_OUTPUT", 0, 10); + } + + public DummyCustomInputOutput(String id, String prefix, int start, int numberOfIOs) { + super(id, // + OpenemsComponent.ChannelId.values(), // + DigitalInput.ChannelId.values(), // + DigitalOutput.ChannelId.values() // + ); + + this.ioChannels = new BooleanWriteChannel[numberOfIOs]; + for (int i = 0; i < numberOfIOs; i++) { + this.ioChannels[i] = (BooleanWriteChannel) this + .addChannel(new ChannelIdImpl(prefix + "_" + (i + start), Doc.of(OpenemsType.BOOLEAN).// + accessMode(AccessMode.READ_WRITE))); + } + } + + @Override + protected DummyCustomInputOutput self() { + return this; + } + + @Override + public BooleanWriteChannel[] digitalOutputChannels() { + return this.ioChannels; + } + + @Override + public BooleanReadChannel[] digitalInputChannels() { + return this.ioChannels; + } + +} diff --git a/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyInputOutput.java b/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyInputOutput.java index f9114bef534..92e6b4a26c1 100644 --- a/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyInputOutput.java +++ b/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyInputOutput.java @@ -1,10 +1,11 @@ package io.openems.edge.io.test; +import java.util.stream.Stream; + import io.openems.common.channel.AccessMode; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.BooleanReadChannel; import io.openems.edge.common.channel.BooleanWriteChannel; -import io.openems.edge.common.channel.ChannelId.ChannelIdImpl; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.test.AbstractDummyOpenemsComponent; @@ -18,25 +19,53 @@ public class DummyInputOutput extends AbstractDummyOpenemsComponent implements DigitalInput, DigitalOutput { - private final BooleanWriteChannel[] ioChannels; + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + INPUT_OUTPUT0(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)), // + INPUT_OUTPUT1(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)), // + INPUT_OUTPUT2(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)), // + INPUT_OUTPUT3(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)), // + INPUT_OUTPUT4(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)), // + INPUT_OUTPUT5(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)), // + INPUT_OUTPUT6(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)), // + INPUT_OUTPUT7(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)), // + INPUT_OUTPUT8(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)), // + INPUT_OUTPUT9(Doc.of(OpenemsType.BOOLEAN) // + .accessMode(AccessMode.READ_WRITE)); - public DummyInputOutput(String id) { - this(id, "INPUT_OUTPUT", 0, 10); + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } } - public DummyInputOutput(String id, String prefix, int start, int numberOfIOs) { + private final BooleanWriteChannel[] digitalOutputChannels; + + public DummyInputOutput(String id) { super(id, // OpenemsComponent.ChannelId.values(), // DigitalInput.ChannelId.values(), // - DigitalOutput.ChannelId.values() // + DigitalOutput.ChannelId.values(), // + ChannelId.values() // ); - - this.ioChannels = new BooleanWriteChannel[numberOfIOs]; - for (int i = 0; i < numberOfIOs; i++) { - this.ioChannels[i] = (BooleanWriteChannel) this - .addChannel(new ChannelIdImpl(prefix + "_" + (i + start), Doc.of(OpenemsType.BOOLEAN).// - accessMode(AccessMode.READ_WRITE))); - } + this.digitalOutputChannels = Stream.of(ChannelId.values()) // + .filter(channelId -> channelId.doc().getAccessMode() == AccessMode.READ_WRITE) // + .map(this::channel) // + .toArray(BooleanWriteChannel[]::new); } @Override @@ -46,12 +75,12 @@ protected DummyInputOutput self() { @Override public BooleanWriteChannel[] digitalOutputChannels() { - return this.ioChannels; + return this.digitalOutputChannels; } @Override public BooleanReadChannel[] digitalInputChannels() { - return this.ioChannels; + return this.digitalOutputChannels; } } diff --git a/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1ImplTest.java b/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1ImplTest.java index 11ef8cd64dc..dabd16527a6 100644 --- a/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1ImplTest.java +++ b/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1ImplTest.java @@ -9,17 +9,14 @@ public class IoFilipowskiMrAo1ImplTest { - private static final String COMPONENT_ID = "component0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new IoFilipowskiMrAo1Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setModbusId(MODBUS_ID) // + .setId("component0") // + .setModbusId("modbus0") // .setRelayContact(AnalogOutput.OUTPUT_1) // .build()) .next(new TestCase()); diff --git a/io.openems.edge.io.gpio/test/io/openems/edge/io/gpio/ModberryCM4Test.java b/io.openems.edge.io.gpio/test/io/openems/edge/io/gpio/ModberryCM4Test.java index 113c39f7c6a..084079c95f0 100644 --- a/io.openems.edge.io.gpio/test/io/openems/edge/io/gpio/ModberryCM4Test.java +++ b/io.openems.edge.io.gpio/test/io/openems/edge/io/gpio/ModberryCM4Test.java @@ -1,5 +1,6 @@ package io.openems.edge.io.gpio; +import static io.openems.edge.io.gpio.hardware.HardwareType.MODBERRY_X500_M40804_W; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -27,14 +28,11 @@ import io.openems.edge.io.gpio.api.AbstractGpioChannel; import io.openems.edge.io.gpio.api.ReadChannelId; import io.openems.edge.io.gpio.api.WriteChannelId; -import io.openems.edge.io.gpio.hardware.HardwareType; public class ModberryCM4Test { private File root; - private static final String ID = "io0"; - private static final List CHANNEL_IDS = List.of(// new ReadChannelId(18, "DigitalInput1"), // new ReadChannelId(19, "DigitalInput2"), // @@ -93,11 +91,11 @@ private void setGpioFile(File root, int gpioNumber, int value) throws IOExceptio @Test public void testChannelIdsAreCorrect() throws Exception { var config = MyConfig.create() // - .setId(ID) // - .setAlias(ID) // + .setId("io0") // + .setAlias("io0") // .setEnabled(true) // .setGpioPath(this.folder.getRoot().getAbsolutePath()) // - .setHardwareType(HardwareType.MODBERRY_X500_M40804_W) // + .setHardwareType(MODBERRY_X500_M40804_W) // .build(); IoGpio modberryComponent = new IoGpioImpl(); new ComponentTest(modberryComponent).activate(config); @@ -107,11 +105,11 @@ public void testChannelIdsAreCorrect() throws Exception { @Test public void testComponentLoadsSucesfully() throws Exception { var config = MyConfig.create() // - .setId(ID) // - .setAlias(ID) // + .setId("io0") // + .setAlias("io0") // .setEnabled(true) // .setGpioPath(this.folder.getRoot().getAbsolutePath()) // - .setHardwareType(HardwareType.MODBERRY_X500_M40804_W) // + .setHardwareType(MODBERRY_X500_M40804_W) // .build(); new ComponentTest(new IoGpioImpl()) // .activate(config); @@ -120,19 +118,19 @@ public void testComponentLoadsSucesfully() throws Exception { @Test public void testInputValuesAreDefault() throws Exception { var config = MyConfig.create() // - .setId(ID) // - .setAlias(ID) // + .setId("io0") // + .setAlias("io0") // .setEnabled(true) // .setGpioPath(this.folder.getRoot().getAbsolutePath()) // - .setHardwareType(HardwareType.MODBERRY_X500_M40804_W) // + .setHardwareType(MODBERRY_X500_M40804_W) // .build(); new ComponentTest(new IoGpioImpl()) // .activate(config) // .next(new TestCase("Default input values are false") // - .output(new ChannelAddress(ID, "DigitalInput1"), false) // - .output(new ChannelAddress(ID, "DigitalInput2"), false) // - .output(new ChannelAddress(ID, "DigitalInput3"), false) // - .output(new ChannelAddress(ID, "DigitalInput4"), false) // + .output(new ChannelAddress("io0", "DigitalInput1"), false) // + .output(new ChannelAddress("io0", "DigitalInput2"), false) // + .output(new ChannelAddress("io0", "DigitalInput3"), false) // + .output(new ChannelAddress("io0", "DigitalInput4"), false) // ); } @@ -143,19 +141,19 @@ public void testChangeOutputWrittenToFs() throws Exception { assertEquals(this.readGpioFile(this.root, 24), "0"); assertEquals(this.readGpioFile(this.root, 25), "0"); var config = MyConfig.create() // - .setId(ID) // - .setAlias(ID) // + .setId("io0") // + .setAlias("io0") // .setEnabled(true) // .setGpioPath(this.folder.getRoot().getAbsolutePath()) // - .setHardwareType(HardwareType.MODBERRY_X500_M40804_W) // + .setHardwareType(MODBERRY_X500_M40804_W) // .build(); new ComponentTest(new IoGpioImpl()) // .activate(config) // .next(new TestCase("Write values are written to fs.") // - .input(new ChannelAddress(ID, "DigitalOutput1"), true) // - .input(new ChannelAddress(ID, "DigitalOutput2"), true) // - .input(new ChannelAddress(ID, "DigitalOutput3"), true) // - .input(new ChannelAddress(ID, "DigitalOutput4"), true) // + .input(new ChannelAddress("io0", "DigitalOutput1"), true) // + .input(new ChannelAddress("io0", "DigitalOutput2"), true) // + .input(new ChannelAddress("io0", "DigitalOutput3"), true) // + .input(new ChannelAddress("io0", "DigitalOutput4"), true) // ); assertEquals(this.readGpioFile(this.root, 22), "1"); assertEquals(this.readGpioFile(this.root, 23), "1"); @@ -171,20 +169,20 @@ public void testChangeInputIsDetected() throws Exception { assertEquals(this.readGpioFile(this.root, 21), "0"); var config = MyConfig.create() // - .setId(ID) // - .setAlias(ID) // + .setId("io0") // + .setAlias("io0") // .setEnabled(true) // .setGpioPath(this.folder.getRoot().getAbsolutePath()) // - .setHardwareType(HardwareType.MODBERRY_X500_M40804_W) // + .setHardwareType(MODBERRY_X500_M40804_W) // .build(); new ComponentTest(new IoGpioImpl()) // .activate(config) // .next(new TestCase("Read values are detected by the component.") // - .output(new ChannelAddress(ID, "DigitalInput1"), false) // - .output(new ChannelAddress(ID, "DigitalInput2"), false) // - .output(new ChannelAddress(ID, "DigitalInput3"), false) // - .output(new ChannelAddress(ID, "DigitalInput4"), false) // + .output(new ChannelAddress("io0", "DigitalInput1"), false) // + .output(new ChannelAddress("io0", "DigitalInput2"), false) // + .output(new ChannelAddress("io0", "DigitalInput3"), false) // + .output(new ChannelAddress("io0", "DigitalInput4"), false) // ); this.setGpioFile(this.root, 18, 1); @@ -200,10 +198,10 @@ public void testChangeInputIsDetected() throws Exception { new ComponentTest(new IoGpioImpl()) // .activate(config) // .next(new TestCase("Read values are detected by the component.") // - .output(new ChannelAddress(ID, "DigitalInput1"), true) // - .output(new ChannelAddress(ID, "DigitalInput2"), true) // - .output(new ChannelAddress(ID, "DigitalInput3"), true) // - .output(new ChannelAddress(ID, "DigitalInput4"), true) // + .output(new ChannelAddress("io0", "DigitalInput1"), true) // + .output(new ChannelAddress("io0", "DigitalInput2"), true) // + .output(new ChannelAddress("io0", "DigitalInput3"), true) // + .output(new ChannelAddress("io0", "DigitalInput4"), true) // ); } @@ -211,18 +209,19 @@ public void testChangeInputIsDetected() throws Exception { public void testJavaApi() throws Exception { this.setGpioFile(this.root, 22, 0); var config = MyConfig.create() // - .setId(ID) // - .setAlias(ID) // + .setId("io0") // + .setAlias("io0") // .setEnabled(true) // .setGpioPath(this.folder.getRoot().getAbsolutePath()) // - .setHardwareType(HardwareType.MODBERRY_X500_M40804_W) // + .setHardwareType(MODBERRY_X500_M40804_W) // .build(); var componentManager = new DummyComponentManager(); - var componentTest = new ComponentTest(new IoGpioImpl()).activate(config); + var componentTest = new ComponentTest(new IoGpioImpl()) // + .activate(config); componentManager.addComponent(componentTest.getSut()); // Get get component channel value as java reference - WriteChannel writeChannel = componentManager.getChannel(new ChannelAddress(ID, "DigitalOutput1")); + WriteChannel writeChannel = componentManager.getChannel(new ChannelAddress("io0", "DigitalOutput1")); assertFalse(writeChannel.value().isDefined()); writeChannel.setNextValue(true); } @@ -231,11 +230,11 @@ public void testJavaApi() throws Exception { public void testInterfaceDigitalOutputChannels() throws Exception { this.setGpioFile(this.root, 22, 0); var config = MyConfig.create() // - .setId(ID) // - .setAlias(ID) // + .setId("io0") // + .setAlias("io0") // .setEnabled(true) // .setGpioPath(this.folder.getRoot().getAbsolutePath()) // - .setHardwareType(HardwareType.MODBERRY_X500_M40804_W) // + .setHardwareType(MODBERRY_X500_M40804_W) // .build(); var componentManager = new DummyComponentManager(); var componentTest = new ComponentTest(new IoGpioImpl()) // @@ -249,11 +248,11 @@ public void testInterfaceDigitalOutputChannels() throws Exception { public void testInterfaceDigitalInputChannels() throws Exception { this.setGpioFile(this.root, 22, 0); var config = MyConfig.create() // - .setId(ID) // - .setAlias(ID) // + .setId("io0") // + .setAlias("io0") // .setEnabled(true) // .setGpioPath(this.folder.getRoot().getAbsolutePath()) // - .setHardwareType(HardwareType.MODBERRY_X500_M40804_W) // + .setHardwareType(MODBERRY_X500_M40804_W) // .build(); var componentManager = new DummyComponentManager(); var componentTest = new ComponentTest(new IoGpioImpl()) // diff --git a/io.openems.edge.io.kmtronic/test/io/openems/edge/io/kmtronic/eight/IoKmtronicRelay8PortImplTest.java b/io.openems.edge.io.kmtronic/test/io/openems/edge/io/kmtronic/eight/IoKmtronicRelay8PortImplTest.java index 67b002d2fae..e27c0302a00 100644 --- a/io.openems.edge.io.kmtronic/test/io/openems/edge/io/kmtronic/eight/IoKmtronicRelay8PortImplTest.java +++ b/io.openems.edge.io.kmtronic/test/io/openems/edge/io/kmtronic/eight/IoKmtronicRelay8PortImplTest.java @@ -8,17 +8,14 @@ public class IoKmtronicRelay8PortImplTest { - private static final String IO_ID = "io0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new IoKmtronicRelay8PortImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(IO_ID) // - .setModbusId(MODBUS_ID) // + .setId("io0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.io.kmtronic/test/io/openems/edge/io/kmtronic/four/IoKmtronicRelay4PortImplTest.java b/io.openems.edge.io.kmtronic/test/io/openems/edge/io/kmtronic/four/IoKmtronicRelay4PortImplTest.java index e7ccd9abf4c..36b4ab28c6f 100644 --- a/io.openems.edge.io.kmtronic/test/io/openems/edge/io/kmtronic/four/IoKmtronicRelay4PortImplTest.java +++ b/io.openems.edge.io.kmtronic/test/io/openems/edge/io/kmtronic/four/IoKmtronicRelay4PortImplTest.java @@ -8,17 +8,14 @@ public class IoKmtronicRelay4PortImplTest { - private static final String IO_ID = "io0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new IoKmtronicRelay4PortImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(IO_ID) // - .setModbusId(MODBUS_ID) // + .setId("io0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.io.offgridswitch/test/io/openems/edge/iooffgridswitch/IoOffGridSwitchImplTest.java b/io.openems.edge.io.offgridswitch/test/io/openems/edge/iooffgridswitch/IoOffGridSwitchImplTest.java index 9d182edd712..0f2944b67f6 100644 --- a/io.openems.edge.io.offgridswitch/test/io/openems/edge/iooffgridswitch/IoOffGridSwitchImplTest.java +++ b/io.openems.edge.io.offgridswitch/test/io/openems/edge/iooffgridswitch/IoOffGridSwitchImplTest.java @@ -2,7 +2,6 @@ import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; @@ -10,29 +9,18 @@ public class IoOffGridSwitchImplTest { - private static final String COMPONENT_ID = "ioOffGridSwitch0"; - - private static final String IO_ID = "io0"; - private static final ChannelAddress INPUT_MAIN_CONTACTOR = new ChannelAddress(IO_ID, "InputOutput0"); - private static final ChannelAddress INPUT_GRID_STATUS = new ChannelAddress(IO_ID, "InputOutput1"); - private static final ChannelAddress INPUT_GROUNDING_CONTACTOR = new ChannelAddress(IO_ID, "InputOutput2"); - private static final ChannelAddress OUTPUT_MAIN_CONTACTOR = new ChannelAddress(IO_ID, "InputOutput3"); - private static final ChannelAddress OUTPUT_GROUNDING_CONTACTOR = new ChannelAddress(IO_ID, "InputOutput4"); - @Test public void test() throws Exception { - var io0 = new DummyInputOutput(IO_ID); - new ComponentTest(new IoOffGridSwitchImpl()) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(io0) // + .addComponent(new DummyInputOutput("io0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setInputMainContactor(INPUT_MAIN_CONTACTOR.toString()) // - .setInputGridStatus(INPUT_GRID_STATUS.toString()) // - .setInputGroundingContactor(INPUT_GROUNDING_CONTACTOR.toString()) // - .setOutputMainContactor(OUTPUT_MAIN_CONTACTOR.toString()) // - .setOutputGroundingContactor(OUTPUT_GROUNDING_CONTACTOR.toString()) // + .setId("ioOffGridSwitch0") // + .setInputMainContactor("io0/InputOutput0") // + .setInputGridStatus("io0/InputOutput1") // + .setInputGroundingContactor("io0/InputOutput2") // + .setOutputMainContactor("io0/InputOutput3") // + .setOutputGroundingContactor("io0/InputOutput4") // .build()) .next(new TestCase()); } diff --git a/io.openems.edge.io.revpi/test/io/openems/edge/io/revpi/IoRevolutionPiDigitalIoImplTest.java b/io.openems.edge.io.revpi/test/io/openems/edge/io/revpi/IoRevolutionPiDigitalIoImplTest.java index db1698ef9a2..7f7285b5b65 100644 --- a/io.openems.edge.io.revpi/test/io/openems/edge/io/revpi/IoRevolutionPiDigitalIoImplTest.java +++ b/io.openems.edge.io.revpi/test/io/openems/edge/io/revpi/IoRevolutionPiDigitalIoImplTest.java @@ -6,8 +6,6 @@ public class IoRevolutionPiDigitalIoImplTest { - // private static final String COMPONENT_ID = "io0"; - @Test public void test() throws Exception { new ComponentTest(new IoRevolutionPiDigitalIoImpl()) // diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly3em/Config.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly3em/Config.java index 5ff09dac9e0..e86f0430f61 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly3em/Config.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly3em/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "IO Shelly 3EM", // diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly3em/IoShelly3EmImpl.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly3em/IoShelly3EmImpl.java index ca9eaf27fcb..d33add0361a 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly3em/IoShelly3EmImpl.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly3em/IoShelly3EmImpl.java @@ -28,6 +28,7 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.http.api.BridgeHttp; import io.openems.edge.bridge.http.api.BridgeHttpFactory; import io.openems.edge.bridge.http.api.HttpResponse; @@ -37,7 +38,6 @@ import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.io.api.DigitalOutput; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/Config.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/Config.java index b3613ab4b7f..d8fc37c4ecd 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/Config.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; import io.openems.edge.meter.api.SinglePhase; @ObjectClassDefinition(// diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/IoShellyPlugImpl.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/IoShellyPlugImpl.java index 2bdad4b9521..0e196afdfc6 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/IoShellyPlugImpl.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/IoShellyPlugImpl.java @@ -20,6 +20,7 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.MeterType; import io.openems.common.utils.JsonUtils; import io.openems.edge.bridge.http.api.BridgeHttp; import io.openems.edge.bridge.http.api.BridgeHttpFactory; @@ -30,7 +31,6 @@ import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.io.api.DigitalOutput; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; import io.openems.edge.meter.api.SinglePhaseMeter; diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplus1pm/Config.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplus1pm/Config.java index 2de9b0a17f0..9317e9e2e4b 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplus1pm/Config.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplus1pm/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; import io.openems.edge.meter.api.SinglePhase; @ObjectClassDefinition(// diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplus1pm/IoShellyPlus1PmImpl.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplus1pm/IoShellyPlus1PmImpl.java index 16bd2cf3af0..2de16729597 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplus1pm/IoShellyPlus1PmImpl.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplus1pm/IoShellyPlus1PmImpl.java @@ -27,6 +27,7 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.http.api.BridgeHttp; import io.openems.edge.bridge.http.api.BridgeHttpFactory; import io.openems.edge.bridge.http.api.HttpResponse; @@ -36,7 +37,6 @@ import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.io.api.DigitalOutput; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; import io.openems.edge.meter.api.SinglePhaseMeter; import io.openems.edge.timedata.api.Timedata; diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplusplugs/Config.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplusplugs/Config.java index 39904456e3f..333aa093ac8 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplusplugs/Config.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplusplugs/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; import io.openems.edge.meter.api.SinglePhase; @ObjectClassDefinition(// diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplusplugs/IoShellyPlusPlugsImpl.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplusplugs/IoShellyPlusPlugsImpl.java index cdd253f120f..4308fed821a 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplusplugs/IoShellyPlusPlugsImpl.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplusplugs/IoShellyPlusPlugsImpl.java @@ -26,6 +26,7 @@ import com.google.gson.JsonElement; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.http.api.BridgeHttp; import io.openems.edge.bridge.http.api.BridgeHttpFactory; import io.openems.edge.bridge.http.api.HttpError; @@ -36,7 +37,6 @@ import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.io.api.DigitalOutput; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; import io.openems.edge.meter.api.SinglePhaseMeter; import io.openems.edge.timedata.api.Timedata; diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/Config.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/Config.java index b594f9b1603..57dfdf4412d 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/Config.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition( name = "IO Shelly Pro 3EM", // diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3Em.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3Em.java index c5899305afe..e545e5bec63 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3Em.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3Em.java @@ -3,8 +3,8 @@ import io.openems.common.channel.Level; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.StateChannel; -import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.component.OpenemsComponent; public interface IoShellyPro3Em extends OpenemsComponent { diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3EmImpl.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3EmImpl.java index 15db1dac66f..e0a59bc939b 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3EmImpl.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3EmImpl.java @@ -25,6 +25,7 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.http.api.BridgeHttp; import io.openems.edge.bridge.http.api.BridgeHttpFactory; import io.openems.edge.bridge.http.api.HttpError; @@ -33,7 +34,6 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly25/IoShelly25ImplTest.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly25/IoShelly25ImplTest.java index 06b3f7975cf..679d091a7de 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly25/IoShelly25ImplTest.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly25/IoShelly25ImplTest.java @@ -1,20 +1,19 @@ package io.openems.edge.io.shelly.shelly25; +import static io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory.ofDummyBridge; + import org.junit.Test; -import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; import io.openems.edge.common.test.ComponentTest; public class IoShelly25ImplTest { - private static final String COMPONENT_ID = "io0"; - @Test public void test() throws Exception { new ComponentTest(new IoShelly25Impl()) // - .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // + .addReference("httpBridgeFactory", ofDummyBridge()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("io0") // .setIp("127.0.0.1") // .build()) // ; diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly3em/IoShelly3EmImplTest.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly3em/IoShelly3EmImplTest.java index 1c06c65f443..e51922918ef 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly3em/IoShelly3EmImplTest.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly3em/IoShelly3EmImplTest.java @@ -1,23 +1,22 @@ package io.openems.edge.io.shelly.shelly3em; +import static io.openems.common.types.MeterType.CONSUMPTION_METERED; +import static io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory.ofDummyBridge; + import org.junit.Test; -import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; import io.openems.edge.common.test.ComponentTest; -import io.openems.edge.meter.api.MeterType; public class IoShelly3EmImplTest { - private static final String COMPONENT_ID = "io0"; - @Test public void test() throws Exception { new ComponentTest(new IoShelly3EmImpl()) // - .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // + .addReference("httpBridgeFactory", ofDummyBridge()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("io0") // .setIp("127.0.0.1") // - .setType(MeterType.CONSUMPTION_METERED) // + .setType(CONSUMPTION_METERED) // .build()) // ; } diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly3em/MyConfig.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly3em/MyConfig.java index 73b5ec2dc44..aa1e32a5ef9 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly3em/MyConfig.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly3em/MyConfig.java @@ -1,7 +1,7 @@ package io.openems.edge.io.shelly.shelly3em; import io.openems.common.test.AbstractComponentConfig; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplug/IoShellyPlugImplTest.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplug/IoShellyPlugImplTest.java index efef443c6b0..60d97be0f50 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplug/IoShellyPlugImplTest.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplug/IoShellyPlugImplTest.java @@ -1,25 +1,24 @@ package io.openems.edge.io.shelly.shellyplug; +import static io.openems.common.types.MeterType.PRODUCTION; +import static io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory.ofDummyBridge; + import org.junit.Test; -import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; import io.openems.edge.common.test.ComponentTest; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; public class IoShellyPlugImplTest { - private static final String COMPONENT_ID = "io0"; - @Test public void test() throws Exception { new ComponentTest(new IoShellyPlugImpl()) // - .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // + .addReference("httpBridgeFactory", ofDummyBridge()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("io0") // .setPhase(SinglePhase.L1) // .setIp("127.0.0.1") // - .setType(MeterType.PRODUCTION) // + .setType(PRODUCTION) // .build()) // ; } diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplug/MyConfig.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplug/MyConfig.java index 3fd0443b386..995335e84d6 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplug/MyConfig.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplug/MyConfig.java @@ -1,7 +1,7 @@ package io.openems.edge.io.shelly.shellyplug; import io.openems.common.test.AbstractComponentConfig; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; import io.openems.edge.meter.api.SinglePhase; @SuppressWarnings("all") diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplus1pm/IoShellyPlus1PmImplTest.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplus1pm/IoShellyPlus1PmImplTest.java index e3443f8d6b1..d2314907aea 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplus1pm/IoShellyPlus1PmImplTest.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplus1pm/IoShellyPlus1PmImplTest.java @@ -1,13 +1,19 @@ package io.openems.edge.io.shelly.shellyplus1pm; -import static io.openems.edge.meter.api.MeterType.CONSUMPTION_METERED; +import static io.openems.common.types.MeterType.CONSUMPTION_METERED; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L1; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L2; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.CURRENT; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.VOLTAGE; import static io.openems.edge.meter.api.SinglePhase.L1; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.bridge.http.api.HttpError; import io.openems.edge.bridge.http.api.HttpResponse; import io.openems.edge.bridge.http.dummy.DummyBridgeHttpBundle; @@ -17,17 +23,6 @@ public class IoShellyPlus1PmImplTest { - private static final String COMPONENT_ID = "io0"; - - private static final ChannelAddress ACTIVE_POWER = new ChannelAddress(COMPONENT_ID, "ActivePower"); - private static final ChannelAddress ACTIVE_POWER_L1 = new ChannelAddress(COMPONENT_ID, "ActivePowerL1"); - private static final ChannelAddress ACTIVE_POWER_L2 = new ChannelAddress(COMPONENT_ID, "ActivePowerL2"); - private static final ChannelAddress CURRENT = new ChannelAddress(COMPONENT_ID, "Current"); - private static final ChannelAddress VOLTAGE = new ChannelAddress(COMPONENT_ID, "Voltage"); - private static final ChannelAddress PRODUCTION_ENERGY = new ChannelAddress(COMPONENT_ID, "ActiveProductionEnergy"); - private static final ChannelAddress CONSUMPTION_ENERGY = new ChannelAddress(COMPONENT_ID, - "ActiveConsumptionEnergy"); - @Test public void test() throws Exception { final var httpTestBundle = new DummyBridgeHttpBundle(); @@ -37,7 +32,7 @@ public void test() throws Exception { .addReference("httpBridgeFactory", httpTestBundle.factory()) // .addReference("timedata", new DummyTimedata("timedata0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("io0") // .setIp("127.0.0.1") // .setType(CONSUMPTION_METERED) // .setPhase(L1) // @@ -122,7 +117,6 @@ public void test() throws Exception { .next(new TestCase("Invalid read response") // .onBeforeControllersCallbacks(() -> assertEquals("Off|123 W", sut.debugLog())) - .onBeforeControllersCallbacks(() -> { httpTestBundle.forceNextFailedResult(HttpError.ResponseError.notFound()); httpTestBundle.triggerNextCycle(); @@ -133,24 +127,19 @@ public void test() throws Exception { .output(CURRENT, null) // .output(VOLTAGE, null) // - .output(PRODUCTION_ENERGY, 0L) // - .output(CONSUMPTION_ENERGY, 0L)) // + .output(ACTIVE_PRODUCTION_ENERGY, 0L) // + .output(ACTIVE_CONSUMPTION_ENERGY, 0L)) // .next(new TestCase("Write") // .onBeforeControllersCallbacks(() -> assertEquals("Unknown|UNDEFINED", sut.debugLog())) - .onBeforeControllersCallbacks(() -> { - sut.setRelay(true); - }) // + .onBeforeControllersCallbacks(() -> sut.setRelay(true)) // .also(testCase -> { final var relayTurnedOn = httpTestBundle.expect("http://127.0.0.1/relay/0?turn=on") .toBeCalled(); - testCase.onBeforeControllersCallbacks(() -> { - httpTestBundle.triggerNextCycle(); - }); - testCase.onAfterWriteCallbacks(() -> { - assertTrue("Failed to turn on relay", relayTurnedOn.get()); - }); + testCase.onBeforeControllersCallbacks(() -> httpTestBundle.triggerNextCycle()); + testCase.onAfterWriteCallbacks( + () -> assertTrue("Failed to turn on relay", relayTurnedOn.get())); })) // .deactivate(); diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplus1pm/MyConfig.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplus1pm/MyConfig.java index e653d439747..cd8ab3621bc 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplus1pm/MyConfig.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplus1pm/MyConfig.java @@ -1,7 +1,7 @@ package io.openems.edge.io.shelly.shellyplus1pm; import io.openems.common.test.AbstractComponentConfig; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; import io.openems.edge.meter.api.SinglePhase; @SuppressWarnings("all") diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplusplugs/IoShellyPlugImplTest.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplusplugs/IoShellyPlugImplTest.java index c91c4c52720..773e6df2661 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplusplugs/IoShellyPlugImplTest.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplusplugs/IoShellyPlugImplTest.java @@ -1,46 +1,40 @@ package io.openems.edge.io.shelly.shellyplusplugs; +import static io.openems.common.types.MeterType.PRODUCTION; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L1; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L2; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.CURRENT; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.VOLTAGE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.bridge.http.api.HttpError; import io.openems.edge.bridge.http.api.HttpResponse; import io.openems.edge.bridge.http.dummy.DummyBridgeHttpBundle; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; import io.openems.edge.timedata.test.DummyTimedata; public class IoShellyPlugImplTest { - private static final String COMPONENT_ID = "io0"; - - private static final ChannelAddress ACTIVE_POWER = new ChannelAddress(COMPONENT_ID, "ActivePower"); - private static final ChannelAddress ACTIVE_POWER_L1 = new ChannelAddress(COMPONENT_ID, "ActivePowerL1"); - private static final ChannelAddress ACTIVE_POWER_L2 = new ChannelAddress(COMPONENT_ID, "ActivePowerL2"); - private static final ChannelAddress CURRENT = new ChannelAddress(COMPONENT_ID, "Current"); - private static final ChannelAddress VOLTAGE = new ChannelAddress(COMPONENT_ID, "Voltage"); - private static final ChannelAddress PRODUCTION_ENERGY = new ChannelAddress(COMPONENT_ID, "ActiveProductionEnergy"); - private static final ChannelAddress CONSUMPTION_ENERGY = new ChannelAddress(COMPONENT_ID, - "ActiveConsumptionEnergy"); - @Test public void test() throws Exception { final var httpTestBundle = new DummyBridgeHttpBundle(); - final var sut = new IoShellyPlusPlugsImpl(); new ComponentTest(sut) // .addReference("httpBridgeFactory", httpTestBundle.factory()) // .addReference("timedata", new DummyTimedata("timedata0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("io0") // .setPhase(SinglePhase.L1) // .setIp("127.0.0.1") // - .setType(MeterType.PRODUCTION) // + .setType(PRODUCTION) // .build()) // .next(new TestCase("Successful read response") // @@ -81,8 +75,8 @@ public void test() throws Exception { .output(CURRENT, null) // .output(VOLTAGE, null) // - .output(PRODUCTION_ENERGY, 0L) // - .output(CONSUMPTION_ENERGY, 0L)) // + .output(ACTIVE_PRODUCTION_ENERGY, 0L) // + .output(ACTIVE_CONSUMPTION_ENERGY, 0L)) // .next(new TestCase("Write") // .onBeforeControllersCallbacks(() -> assertEquals("Unknown|UNDEFINED", sut.debugLog())) @@ -93,12 +87,9 @@ public void test() throws Exception { final var relayTurnedOn = httpTestBundle.expect("http://127.0.0.1/relay/0?turn=on") .toBeCalled(); - testCase.onBeforeControllersCallbacks(() -> { - httpTestBundle.triggerNextCycle(); - }); - testCase.onAfterWriteCallbacks(() -> { - assertTrue("Failed to turn on relay", relayTurnedOn.get()); - }); + testCase.onBeforeControllersCallbacks(() -> httpTestBundle.triggerNextCycle()); + testCase.onAfterWriteCallbacks( + () -> assertTrue("Failed to turn on relay", relayTurnedOn.get())); })) // .deactivate(); diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplusplugs/MyConfig.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplusplugs/MyConfig.java index 81fe32540f0..3f006a2ad90 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplusplugs/MyConfig.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellyplusplugs/MyConfig.java @@ -1,8 +1,7 @@ package io.openems.edge.io.shelly.shellyplusplugs; import io.openems.common.test.AbstractComponentConfig; -import io.openems.edge.io.shelly.shellyplusplugs.Config; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; import io.openems.edge.meter.api.SinglePhase; @SuppressWarnings("all") diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/IoShellyPro3ImplTest.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/IoShellyPro3ImplTest.java index ed4f8d960b6..c7213418efe 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/IoShellyPro3ImplTest.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/IoShellyPro3ImplTest.java @@ -7,14 +7,12 @@ public class IoShellyPro3ImplTest { - private static final String COMPONENT_ID = "io0"; - @Test public void test() throws Exception { new ComponentTest(new IoShellyPro3Impl()) // .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("io0") // .setIp("127.0.0.1") // .build()) // ; diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3em/IoShelly3EmImplTest.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3em/IoShelly3EmImplTest.java index 29b0136a7a6..c6f0e340da5 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3em/IoShelly3EmImplTest.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3em/IoShelly3EmImplTest.java @@ -2,20 +2,18 @@ import org.junit.Test; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; import io.openems.edge.common.test.ComponentTest; -import io.openems.edge.meter.api.MeterType; public class IoShelly3EmImplTest { - private static final String COMPONENT_ID = "io0"; - @Test public void test() throws Exception { new ComponentTest(new IoShellyPro3EmImpl()) // .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("io0") // .setIp("127.0.0.1") // .setType(MeterType.CONSUMPTION_METERED) // .build()) // diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3em/MyConfig.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3em/MyConfig.java index 68261ee3841..3f524cfd4cf 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3em/MyConfig.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3em/MyConfig.java @@ -1,7 +1,7 @@ package io.openems.edge.io.shelly.shellypro3em; import io.openems.common.test.AbstractComponentConfig; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.io.wago/test/io/openems/edge/wago/IoWagoImplTest.java b/io.openems.edge.io.wago/test/io/openems/edge/wago/IoWagoImplTest.java index 012eac53fd3..292deeff3f3 100644 --- a/io.openems.edge.io.wago/test/io/openems/edge/wago/IoWagoImplTest.java +++ b/io.openems.edge.io.wago/test/io/openems/edge/wago/IoWagoImplTest.java @@ -16,9 +16,6 @@ public class IoWagoImplTest { - private static final String IO_ID = "io0"; - private static final String MODBUS_ID = "modbus0"; - /** * This is an example "ea-config.xml" downloaded from a WAGO Fieldbus coupler. */ @@ -56,14 +53,14 @@ public void test() throws Exception { var sut = new IoWagoImpl(); new ComponentTest(sut) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID).withIpAddress("127.0.0.1")) // + .addReference("setModbus", new DummyModbusBridge("modbus0") // + .withIpAddress("127.0.0.1")) // .activate(MyConfig.create() // - .setId(IO_ID) // - .setModbusId(MODBUS_ID) // + .setId("io0") // + .setModbusId("modbus0") // .setUsername("foo") // .setPassword("bar") // - .build()) // - ; + .build()); InputStream dummyXml = new ByteArrayInputStream(EA_CONFIG.getBytes()); var doc = IoWagoImpl.parseXmlToDocument(dummyXml); diff --git a/io.openems.edge.io.weidmueller/test/io/openems/edge/io/weidmueller/IoWeidmuellerUr20ImplTest.java b/io.openems.edge.io.weidmueller/test/io/openems/edge/io/weidmueller/IoWeidmuellerUr20ImplTest.java index 7a8da19e227..eadbf247bb5 100644 --- a/io.openems.edge.io.weidmueller/test/io/openems/edge/io/weidmueller/IoWeidmuellerUr20ImplTest.java +++ b/io.openems.edge.io.weidmueller/test/io/openems/edge/io/weidmueller/IoWeidmuellerUr20ImplTest.java @@ -9,17 +9,14 @@ public class IoWeidmuellerUr20ImplTest { - private static final String COMPONENT_ID = "io0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new IoWeidmuellerUr20Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setModbusId(MODBUS_ID) // + .setId("io0") // + .setModbusId("modbus0") // .build()) .next(new TestCase()); } diff --git a/io.openems.edge.kaco.blueplanet.hybrid10/src/io/openems/edge/kaco/blueplanet/hybrid10/vectis/KacoBlueplanetHybrid10GridMeterImpl.java b/io.openems.edge.kaco.blueplanet.hybrid10/src/io/openems/edge/kaco/blueplanet/hybrid10/vectis/KacoBlueplanetHybrid10GridMeterImpl.java index aa8535f91a9..e65e7514c1e 100644 --- a/io.openems.edge.kaco.blueplanet.hybrid10/src/io/openems/edge/kaco/blueplanet/hybrid10/vectis/KacoBlueplanetHybrid10GridMeterImpl.java +++ b/io.openems.edge.kaco.blueplanet.hybrid10/src/io/openems/edge/kaco/blueplanet/hybrid10/vectis/KacoBlueplanetHybrid10GridMeterImpl.java @@ -17,12 +17,12 @@ import org.osgi.service.event.propertytypes.EventTopics; import org.osgi.service.metatype.annotations.Designate; +import io.openems.common.types.MeterType; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.kaco.blueplanet.hybrid10.core.KacoBlueplanetHybrid10Core; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/core/KacoBlueplanetHybrid10CoreImplTest.java b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/core/KacoBlueplanetHybrid10CoreImplTest.java index e0d6d9bbb72..bf8f9736926 100644 --- a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/core/KacoBlueplanetHybrid10CoreImplTest.java +++ b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/core/KacoBlueplanetHybrid10CoreImplTest.java @@ -6,13 +6,11 @@ public class KacoBlueplanetHybrid10CoreImplTest { - private static final String CORE_ID = "kacoCore0"; - @Test public void test() throws Exception { new ComponentTest(new KacoBlueplanetHybrid10CoreImpl()) // .activate(MyConfig.create() // - .setId(CORE_ID) // + .setId("kacoCore0") // .setIdentkey("") // .setIp("192.168.0.1") // .setSerialnumber("123456") // diff --git a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/ess/KacoBlueplanetHybrid10EssImplTest.java b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/ess/KacoBlueplanetHybrid10EssImplTest.java index cb4ec7953a9..f06f0bbafb1 100644 --- a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/ess/KacoBlueplanetHybrid10EssImplTest.java +++ b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/ess/KacoBlueplanetHybrid10EssImplTest.java @@ -8,18 +8,14 @@ public class KacoBlueplanetHybrid10EssImplTest { - private static final String ESS_ID = "ess0"; - private static final String CORE_ID = "kacoCore0"; - private static final String TIMEDATA_ID = "timedata0"; - @Test public void test() throws Exception { new ComponentTest(new KacoBlueplanetHybrid10EssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("timedata", new DummyTimedata(TIMEDATA_ID)) // + .addReference("timedata", new DummyTimedata("timedata0")) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setCoreId(CORE_ID) // + .setId("ess0") // + .setCoreId("kacoCore0") // .build()) // ; } diff --git a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/ess/charger/KacoBlueplanetHybrid10ChargerImplTest.java b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/ess/charger/KacoBlueplanetHybrid10ChargerImplTest.java index 6d9606bb0b6..e5d00f7b9e2 100644 --- a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/ess/charger/KacoBlueplanetHybrid10ChargerImplTest.java +++ b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/ess/charger/KacoBlueplanetHybrid10ChargerImplTest.java @@ -7,16 +7,13 @@ public class KacoBlueplanetHybrid10ChargerImplTest { - private static final String CHARGER_ID = "charger0"; - private static final String CORE_ID = "kacoCore0"; - @Test public void test() throws Exception { new ComponentTest(new KacoBlueplanetHybrid10ChargerImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .activate(MyConfig.create() // - .setId(CHARGER_ID) // - .setCoreId(CORE_ID) // + .setId("charger0") // + .setCoreId("kacoCore0") // .build()) // ; } diff --git a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/pvinverter/KacoBlueplanetHybrid10PvInverterImplTest.java b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/pvinverter/KacoBlueplanetHybrid10PvInverterImplTest.java index 2a34e01ddaa..a8c9ee17853 100644 --- a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/pvinverter/KacoBlueplanetHybrid10PvInverterImplTest.java +++ b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/pvinverter/KacoBlueplanetHybrid10PvInverterImplTest.java @@ -7,16 +7,13 @@ public class KacoBlueplanetHybrid10PvInverterImplTest { - private static final String PV_INVERTER_ID = "pvInverter0"; - private static final String CORE_ID = "kacoCore0"; - @Test public void test() throws Exception { new ComponentTest(new KacoBlueplanetHybrid10PvInverterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .activate(MyConfig.create() // - .setId(PV_INVERTER_ID) // - .setCoreId(CORE_ID) // + .setId("pvInverter0") // + .setCoreId("kacoCore0") // .build()) // ; } diff --git a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/vectis/KacoBlueplanetHybrid10GridMeterImplTest.java b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/vectis/KacoBlueplanetHybrid10GridMeterImplTest.java index c4d71724b33..cc50bb9d0fd 100644 --- a/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/vectis/KacoBlueplanetHybrid10GridMeterImplTest.java +++ b/io.openems.edge.kaco.blueplanet.hybrid10/test/io/openems/edge/kaco/blueplanet/hybrid10/vectis/KacoBlueplanetHybrid10GridMeterImplTest.java @@ -7,16 +7,13 @@ public class KacoBlueplanetHybrid10GridMeterImplTest { - private static final String METER_ID = "meter0"; - private static final String CORE_ID = "kacoCore0"; - @Test public void test() throws Exception { new ComponentTest(new KacoBlueplanetHybrid10GridMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setCoreId(CORE_ID) // + .setId("meter0") // + .setCoreId("kacoCore0") // .build()) // ; } diff --git a/io.openems.edge.kostal.piko/src/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImpl.java b/io.openems.edge.kostal.piko/src/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImpl.java index 609d45c34cc..d906d61b56e 100644 --- a/io.openems.edge.kostal.piko/src/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImpl.java +++ b/io.openems.edge.kostal.piko/src/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImpl.java @@ -15,12 +15,12 @@ import org.osgi.service.event.propertytypes.EventTopics; import org.osgi.service.metatype.annotations.Designate; +import io.openems.common.types.MeterType; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.kostal.piko.core.api.KostalPikoCore; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/charger/KostalPikoChargerImplTest.java b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/charger/KostalPikoChargerImplTest.java index ed2b7b7d5cc..96cbaf11cdd 100644 --- a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/charger/KostalPikoChargerImplTest.java +++ b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/charger/KostalPikoChargerImplTest.java @@ -8,15 +8,13 @@ public class KostalPikoChargerImplTest { - private static final String COMPONENT_ID = "charger0"; - @Test public void test() throws Exception { new ComponentTest(new KostalPikoChargerImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // # .addReference("setCore", new KostalPikoCoreImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("charger0") // .setCoreId("core0") // .build()) // ; diff --git a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/core/impl/KostalPikoCoreImplTest.java b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/core/impl/KostalPikoCoreImplTest.java index 5dab51e4a65..dff15af4a7e 100644 --- a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/core/impl/KostalPikoCoreImplTest.java +++ b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/core/impl/KostalPikoCoreImplTest.java @@ -6,13 +6,11 @@ public class KostalPikoCoreImplTest { - private static final String COMPONENT_ID = "core0"; - @Test public void test() throws Exception { new ComponentTest(new KostalPikoCoreImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("core0") // .setIp("127.0.0.1") // .setPort(81) // .setUnitID(0xff) // diff --git a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/ess/KostalPikoEssImplTest.java b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/ess/KostalPikoEssImplTest.java index 1c3e78bfc06..aaeaf69fefa 100644 --- a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/ess/KostalPikoEssImplTest.java +++ b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/ess/KostalPikoEssImplTest.java @@ -8,15 +8,13 @@ public class KostalPikoEssImplTest { - private static final String COMPONENT_ID = "ess0"; - @Test public void test() throws Exception { new ComponentTest(new KostalPikoEssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // # .addReference("setCore", new KostalPikoCoreImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("ess0") // .setCoreId("core0") // .build()) // ; diff --git a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImplTest.java b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImplTest.java index 7895e4c50ed..aec78af51d6 100644 --- a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImplTest.java +++ b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImplTest.java @@ -8,18 +8,18 @@ public class KostalPikoGridMeterImplTest { - private static final String COMPONENT_ID = "meter0"; - @Test public void test() throws Exception { new ComponentTest(new KostalPikoGridMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // # .addReference("setCore", new KostalPikoCoreImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("meter0") // .setCoreId("core0") // - .build()) // - ; + .build()); // + // TODO This does not work because this.worker == null + // .next(new TestCase()) // + // deactivate(); } } diff --git a/io.openems.edge.meter.abb/src/io/openems/edge/meter/abb/b32/Config.java b/io.openems.edge.meter.abb/src/io/openems/edge/meter/abb/b32/Config.java index 707447bbb31..bff5dd7b2fc 100644 --- a/io.openems.edge.meter.abb/src/io/openems/edge/meter/abb/b32/Config.java +++ b/io.openems.edge.meter.abb/src/io/openems/edge/meter/abb/b32/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter ABB B23 M-Bus", // diff --git a/io.openems.edge.meter.abb/src/io/openems/edge/meter/abb/b32/MeterAbbB23Impl.java b/io.openems.edge.meter.abb/src/io/openems/edge/meter/abb/b32/MeterAbbB23Impl.java index a5b30875667..67c78c5ad7f 100644 --- a/io.openems.edge.meter.abb/src/io/openems/edge/meter/abb/b32/MeterAbbB23Impl.java +++ b/io.openems.edge.meter.abb/src/io/openems/edge/meter/abb/b32/MeterAbbB23Impl.java @@ -12,6 +12,7 @@ import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.Designate; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.mbus.api.AbstractOpenemsMbusComponent; import io.openems.edge.bridge.mbus.api.BridgeMbus; import io.openems.edge.bridge.mbus.api.ChannelRecord; @@ -19,7 +20,6 @@ import io.openems.edge.bridge.mbus.api.MbusTask; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MeterAbbB23ImplTest.java b/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MeterAbbB23ImplTest.java index d76e662c8aa..367d1f97dd5 100644 --- a/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MeterAbbB23ImplTest.java +++ b/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MeterAbbB23ImplTest.java @@ -1,29 +1,28 @@ package io.openems.edge.meter.abb.b32; -import java.lang.reflect.InvocationTargetException; - import org.junit.Test; +import io.openems.common.types.MeterType; +import io.openems.common.utils.ReflectionUtils.ReflectionException; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterAbbB23ImplTest { - private static final String COMPONENT_ID = "meter0"; - - @Test(expected = InvocationTargetException.class) + @Test(expected = ReflectionException.class) public void test() throws Exception { new ComponentTest(new MeterAbbB23Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // # .addReference("mbus", null) // TODO create DummyMbusBridge .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("meter0") // .setMbusId("bridge0") // .setPrimaryAddress(10) // .setType(MeterType.PRODUCTION) // .build()) // - ; + .next(new TestCase()) // + .deactivate(); } } diff --git a/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MyConfig.java b/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MyConfig.java index c6c2482aa3f..ffd21f9f547 100644 --- a/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MyConfig.java +++ b/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MyConfig.java @@ -1,7 +1,7 @@ package io.openems.edge.meter.abb.b32; import io.openems.common.test.AbstractComponentConfig; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.api/src/io/openems/edge/meter/api/ElectricityMeter.java b/io.openems.edge.meter.api/src/io/openems/edge/meter/api/ElectricityMeter.java index 5644df4853a..af1c088c8ec 100644 --- a/io.openems.edge.meter.api/src/io/openems/edge/meter/api/ElectricityMeter.java +++ b/io.openems.edge.meter.api/src/io/openems/edge/meter/api/ElectricityMeter.java @@ -7,6 +7,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.channel.PersistencePriority; import io.openems.common.channel.Unit; +import io.openems.common.types.MeterType; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.IntegerDoc; diff --git a/io.openems.edge.meter.api/src/io/openems/edge/meter/test/AbstractDummyElectricityMeter.java b/io.openems.edge.meter.api/src/io/openems/edge/meter/test/AbstractDummyElectricityMeter.java index f16c98cbad3..92aedece446 100644 --- a/io.openems.edge.meter.api/src/io/openems/edge/meter/test/AbstractDummyElectricityMeter.java +++ b/io.openems.edge.meter.api/src/io/openems/edge/meter/test/AbstractDummyElectricityMeter.java @@ -1,10 +1,10 @@ package io.openems.edge.meter.test; +import io.openems.common.types.MeterType; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.test.TestUtils; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; public abstract class AbstractDummyElectricityMeter> extends AbstractOpenemsComponent implements ElectricityMeter { diff --git a/io.openems.edge.meter.artemes.am2/src/io/openems/edge/meter/artemes/am2/Config.java b/io.openems.edge.meter.artemes.am2/src/io/openems/edge/meter/artemes/am2/Config.java index f7fff701591..0783bc43fcd 100644 --- a/io.openems.edge.meter.artemes.am2/src/io/openems/edge/meter/artemes/am2/Config.java +++ b/io.openems.edge.meter.artemes.am2/src/io/openems/edge/meter/artemes/am2/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(name = "Meter Artemes AM-2", description = "Implements the Artemes AM-2 meter.") @interface Config { diff --git a/io.openems.edge.meter.artemes.am2/src/io/openems/edge/meter/artemes/am2/MeterArtemesAM2Impl.java b/io.openems.edge.meter.artemes.am2/src/io/openems/edge/meter/artemes/am2/MeterArtemesAM2Impl.java index 52db9a3acd0..6a09f0e836a 100644 --- a/io.openems.edge.meter.artemes.am2/src/io/openems/edge/meter/artemes/am2/MeterArtemesAM2Impl.java +++ b/io.openems.edge.meter.artemes.am2/src/io/openems/edge/meter/artemes/am2/MeterArtemesAM2Impl.java @@ -17,6 +17,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -32,7 +33,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.artemes.am2/test/io/openems/edge/meter/artemes/am2/MeterArtemesAM2ImplTest.java b/io.openems.edge.meter.artemes.am2/test/io/openems/edge/meter/artemes/am2/MeterArtemesAM2ImplTest.java index cc287731b19..139d9ebe3dd 100644 --- a/io.openems.edge.meter.artemes.am2/test/io/openems/edge/meter/artemes/am2/MeterArtemesAM2ImplTest.java +++ b/io.openems.edge.meter.artemes.am2/test/io/openems/edge/meter/artemes/am2/MeterArtemesAM2ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.artemes.am2; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterArtemesAM2ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterArtemesAM2Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.artemes.am2/test/io/openems/edge/meter/artemes/am2/MyConfig.java b/io.openems.edge.meter.artemes.am2/test/io/openems/edge/meter/artemes/am2/MyConfig.java index 2d1f906e3eb..8d115e37de7 100644 --- a/io.openems.edge.meter.artemes.am2/test/io/openems/edge/meter/artemes/am2/MyConfig.java +++ b/io.openems.edge.meter.artemes.am2/test/io/openems/edge/meter/artemes/am2/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.artemes.am2; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.bcontrol.em300/src/io/openems/edge/meter/bcontrol/em300/Config.java b/io.openems.edge.meter.bcontrol.em300/src/io/openems/edge/meter/bcontrol/em300/Config.java index 8358d843460..eed040fa1f5 100644 --- a/io.openems.edge.meter.bcontrol.em300/src/io/openems/edge/meter/bcontrol/em300/Config.java +++ b/io.openems.edge.meter.bcontrol.em300/src/io/openems/edge/meter/bcontrol/em300/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter B-Control EM300", // diff --git a/io.openems.edge.meter.bcontrol.em300/src/io/openems/edge/meter/bcontrol/em300/MeterBControlEM300Impl.java b/io.openems.edge.meter.bcontrol.em300/src/io/openems/edge/meter/bcontrol/em300/MeterBControlEM300Impl.java index 3cd11ad580e..dd80b522c6d 100644 --- a/io.openems.edge.meter.bcontrol.em300/src/io/openems/edge/meter/bcontrol/em300/MeterBControlEM300Impl.java +++ b/io.openems.edge.meter.bcontrol.em300/src/io/openems/edge/meter/bcontrol/em300/MeterBControlEM300Impl.java @@ -18,6 +18,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -33,7 +34,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.bcontrol.em300/test/io/openems/edge/meter/bcontrol/em300/MeterBControlEM300ImplTest.java b/io.openems.edge.meter.bcontrol.em300/test/io/openems/edge/meter/bcontrol/em300/MeterBControlEM300ImplTest.java index 04513e888fc..ae620f78b1d 100644 --- a/io.openems.edge.meter.bcontrol.em300/test/io/openems/edge/meter/bcontrol/em300/MeterBControlEM300ImplTest.java +++ b/io.openems.edge.meter.bcontrol.em300/test/io/openems/edge/meter/bcontrol/em300/MeterBControlEM300ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.bcontrol.em300; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterBControlEM300ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterBControlEM300Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.bcontrol.em300/test/io/openems/edge/meter/bcontrol/em300/MyConfig.java b/io.openems.edge.meter.bcontrol.em300/test/io/openems/edge/meter/bcontrol/em300/MyConfig.java index 6b5871b6acb..d7fea6d3e1b 100644 --- a/io.openems.edge.meter.bcontrol.em300/test/io/openems/edge/meter/bcontrol/em300/MyConfig.java +++ b/io.openems.edge.meter.bcontrol.em300/test/io/openems/edge/meter/bcontrol/em300/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.bcontrol.em300; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.bgetech/src/io/openems/edge/meter/bgetech/Config.java b/io.openems.edge.meter.bgetech/src/io/openems/edge/meter/bgetech/Config.java index e7dbd3e2af7..d5758485822 100644 --- a/io.openems.edge.meter.bgetech/src/io/openems/edge/meter/bgetech/Config.java +++ b/io.openems.edge.meter.bgetech/src/io/openems/edge/meter/bgetech/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter B+G E-Tech DRT428M-2", // diff --git a/io.openems.edge.meter.bgetech/src/io/openems/edge/meter/bgetech/MeterBgeTechDrt428M2Impl.java b/io.openems.edge.meter.bgetech/src/io/openems/edge/meter/bgetech/MeterBgeTechDrt428M2Impl.java index 39b7909110c..891bc72d879 100644 --- a/io.openems.edge.meter.bgetech/src/io/openems/edge/meter/bgetech/MeterBgeTechDrt428M2Impl.java +++ b/io.openems.edge.meter.bgetech/src/io/openems/edge/meter/bgetech/MeterBgeTechDrt428M2Impl.java @@ -17,6 +17,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -28,7 +29,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.bgetech/test/io/openems/edge/meter/bgetech/MeterBgeTechDrt428M2ImplTest.java b/io.openems.edge.meter.bgetech/test/io/openems/edge/meter/bgetech/MeterBgeTechDrt428M2ImplTest.java index bf6b31f1e20..3cdaaf33098 100644 --- a/io.openems.edge.meter.bgetech/test/io/openems/edge/meter/bgetech/MeterBgeTechDrt428M2ImplTest.java +++ b/io.openems.edge.meter.bgetech/test/io/openems/edge/meter/bgetech/MeterBgeTechDrt428M2ImplTest.java @@ -2,10 +2,11 @@ import org.junit.Test; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterBgeTechDrt428M2ImplTest { @@ -22,6 +23,7 @@ public void testActivatePositive() throws Exception { .setModbusId(MODBUS_ID) // .setType(MeterType.GRID) // .build()) // - ; + .next(new TestCase()) // + .deactivate(); } } \ No newline at end of file diff --git a/io.openems.edge.meter.bgetech/test/io/openems/edge/meter/bgetech/MyConfig.java b/io.openems.edge.meter.bgetech/test/io/openems/edge/meter/bgetech/MyConfig.java index d8200980d6e..cad24e35282 100644 --- a/io.openems.edge.meter.bgetech/test/io/openems/edge/meter/bgetech/MyConfig.java +++ b/io.openems.edge.meter.bgetech/test/io/openems/edge/meter/bgetech/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.bgetech; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.camillebauer.aplus/src/io/openems/edge/meter/camillebauer/aplus/Config.java b/io.openems.edge.meter.camillebauer.aplus/src/io/openems/edge/meter/camillebauer/aplus/Config.java index d5a72ae9cc2..013f474e3fd 100644 --- a/io.openems.edge.meter.camillebauer.aplus/src/io/openems/edge/meter/camillebauer/aplus/Config.java +++ b/io.openems.edge.meter.camillebauer.aplus/src/io/openems/edge/meter/camillebauer/aplus/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Camille Bauer APLUS", // diff --git a/io.openems.edge.meter.camillebauer.aplus/src/io/openems/edge/meter/camillebauer/aplus/MeterCamillebauerAplusImpl.java b/io.openems.edge.meter.camillebauer.aplus/src/io/openems/edge/meter/camillebauer/aplus/MeterCamillebauerAplusImpl.java index 77bd4a98c39..c5596bad717 100644 --- a/io.openems.edge.meter.camillebauer.aplus/src/io/openems/edge/meter/camillebauer/aplus/MeterCamillebauerAplusImpl.java +++ b/io.openems.edge.meter.camillebauer.aplus/src/io/openems/edge/meter/camillebauer/aplus/MeterCamillebauerAplusImpl.java @@ -21,6 +21,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -35,7 +36,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.meter.camillebauer.aplus/test/io/openems/edge/meter/camillebauer/aplus/MeterCamillebauerAplusImplTest.java b/io.openems.edge.meter.camillebauer.aplus/test/io/openems/edge/meter/camillebauer/aplus/MeterCamillebauerAplusImplTest.java index a6ad0bff683..00a6eac5e63 100644 --- a/io.openems.edge.meter.camillebauer.aplus/test/io/openems/edge/meter/camillebauer/aplus/MeterCamillebauerAplusImplTest.java +++ b/io.openems.edge.meter.camillebauer.aplus/test/io/openems/edge/meter/camillebauer/aplus/MeterCamillebauerAplusImplTest.java @@ -1,27 +1,27 @@ package io.openems.edge.meter.camillebauer.aplus; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterCamillebauerAplusImplTest { - private static final String COMPONENT_ID = "component0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterCamillebauerAplusImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setModbusId(MODBUS_ID) // - .setMeterType(MeterType.GRID).setInvert(false).build()) + .setId("component0") // + .setModbusId("modbus0") // + .setMeterType(GRID) // + .setInvert(false) // + .build()) .next(new TestCase()); } diff --git a/io.openems.edge.meter.camillebauer.aplus/test/io/openems/edge/meter/camillebauer/aplus/MyConfig.java b/io.openems.edge.meter.camillebauer.aplus/test/io/openems/edge/meter/camillebauer/aplus/MyConfig.java index 7f53bea4a47..cb5f6b2612a 100644 --- a/io.openems.edge.meter.camillebauer.aplus/test/io/openems/edge/meter/camillebauer/aplus/MyConfig.java +++ b/io.openems.edge.meter.camillebauer.aplus/test/io/openems/edge/meter/camillebauer/aplus/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.camillebauer.aplus; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.carlo.gavazzi.em300/src/io/openems/edge/meter/carlo/gavazzi/em300/Config.java b/io.openems.edge.meter.carlo.gavazzi.em300/src/io/openems/edge/meter/carlo/gavazzi/em300/Config.java index 2f90c6c6014..9ca7fffec9a 100644 --- a/io.openems.edge.meter.carlo.gavazzi.em300/src/io/openems/edge/meter/carlo/gavazzi/em300/Config.java +++ b/io.openems.edge.meter.carlo.gavazzi.em300/src/io/openems/edge/meter/carlo/gavazzi/em300/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Carlo Gavazzi EM300", // diff --git a/io.openems.edge.meter.carlo.gavazzi.em300/src/io/openems/edge/meter/carlo/gavazzi/em300/MeterCarloGavazziEm300Impl.java b/io.openems.edge.meter.carlo.gavazzi.em300/src/io/openems/edge/meter/carlo/gavazzi/em300/MeterCarloGavazziEm300Impl.java index d5aea0a4d23..e7ca0984fcb 100644 --- a/io.openems.edge.meter.carlo.gavazzi.em300/src/io/openems/edge/meter/carlo/gavazzi/em300/MeterCarloGavazziEm300Impl.java +++ b/io.openems.edge.meter.carlo.gavazzi.em300/src/io/openems/edge/meter/carlo/gavazzi/em300/MeterCarloGavazziEm300Impl.java @@ -16,6 +16,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -28,7 +29,6 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.carlo.gavazzi.em300/test/io/openems/edge/meter/carlo/gavazzi/em300/MeterCarloGavazziEm300ImplTest.java b/io.openems.edge.meter.carlo.gavazzi.em300/test/io/openems/edge/meter/carlo/gavazzi/em300/MeterCarloGavazziEm300ImplTest.java index c628829fba6..0e437005611 100644 --- a/io.openems.edge.meter.carlo.gavazzi.em300/test/io/openems/edge/meter/carlo/gavazzi/em300/MeterCarloGavazziEm300ImplTest.java +++ b/io.openems.edge.meter.carlo.gavazzi.em300/test/io/openems/edge/meter/carlo/gavazzi/em300/MeterCarloGavazziEm300ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.carlo.gavazzi.em300; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterCarloGavazziEm300ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterCarloGavazziEm300Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.carlo.gavazzi.em300/test/io/openems/edge/meter/carlo/gavazzi/em300/MyConfig.java b/io.openems.edge.meter.carlo.gavazzi.em300/test/io/openems/edge/meter/carlo/gavazzi/em300/MyConfig.java index df5f25f8945..288ccadea9d 100644 --- a/io.openems.edge.meter.carlo.gavazzi.em300/test/io/openems/edge/meter/carlo/gavazzi/em300/MyConfig.java +++ b/io.openems.edge.meter.carlo.gavazzi.em300/test/io/openems/edge/meter/carlo/gavazzi/em300/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.carlo.gavazzi.em300; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/Config.java b/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/Config.java index 1e99efd16a5..9b158635fe3 100644 --- a/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/Config.java +++ b/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/Config.java @@ -4,7 +4,7 @@ import org.osgi.service.metatype.annotations.AttributeType; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Discovergy", // diff --git a/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/DiscovergyWorker.java b/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/DiscovergyWorker.java index 3be60cedf0d..8f78e4ff125 100644 --- a/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/DiscovergyWorker.java +++ b/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/DiscovergyWorker.java @@ -136,6 +136,7 @@ protected void forever() throws OpenemsNamedException { break; case CONSUMPTION_NOT_METERED: // to be validated case CONSUMPTION_METERED: // to be validated + case MANAGED_CONSUMPTION_METERED: case PRODUCTION_AND_CONSUMPTION: case PRODUCTION: this.parent._setActivePower(TypeUtils.multiply(activePower, -1)); // invert diff --git a/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/MeterDiscovergyImpl.java b/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/MeterDiscovergyImpl.java index 24884b6d2d6..c69107723a9 100644 --- a/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/MeterDiscovergyImpl.java +++ b/io.openems.edge.meter.discovergy/src/io/openems/edge/meter/discovergy/MeterDiscovergyImpl.java @@ -17,6 +17,7 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.MeterType; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; @@ -26,7 +27,6 @@ import io.openems.edge.common.jsonapi.JsonApiBuilder; import io.openems.edge.common.user.User; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.discovergy.jsonrpc.Field; import io.openems.edge.meter.discovergy.jsonrpc.GetFieldNamesRequest; import io.openems.edge.meter.discovergy.jsonrpc.GetFieldNamesResponse; diff --git a/io.openems.edge.meter.discovergy/test/io/openems/edge/meter/discovergy/MeterDiscovergyImplTest.java b/io.openems.edge.meter.discovergy/test/io/openems/edge/meter/discovergy/MeterDiscovergyImplTest.java index 5ae02ad71c1..90dbcfe4cd6 100644 --- a/io.openems.edge.meter.discovergy/test/io/openems/edge/meter/discovergy/MeterDiscovergyImplTest.java +++ b/io.openems.edge.meter.discovergy/test/io/openems/edge/meter/discovergy/MeterDiscovergyImplTest.java @@ -1,20 +1,19 @@ package io.openems.edge.meter.discovergy; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.common.test.ComponentTest; -import io.openems.edge.meter.api.MeterType; public class MeterDiscovergyImplTest { - private static final String COMPONENT_ID = "meter0"; - @Test public void test() throws Exception { new ComponentTest(new MeterDiscovergyImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setType(GRID) // .setPassword("xxx") // .setEmail("x@y.z") // .setSerialNumber("12345678") // diff --git a/io.openems.edge.meter.discovergy/test/io/openems/edge/meter/discovergy/MyConfig.java b/io.openems.edge.meter.discovergy/test/io/openems/edge/meter/discovergy/MyConfig.java index f3010303879..5f0c14b9ee4 100644 --- a/io.openems.edge.meter.discovergy/test/io/openems/edge/meter/discovergy/MyConfig.java +++ b/io.openems.edge.meter.discovergy/test/io/openems/edge/meter/discovergy/MyConfig.java @@ -1,7 +1,7 @@ package io.openems.edge.meter.discovergy; import io.openems.common.test.AbstractComponentConfig; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm120/Config.java b/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm120/Config.java index 1800860d9c5..66b6e54879d 100644 --- a/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm120/Config.java +++ b/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm120/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; import io.openems.edge.meter.api.SinglePhase; @ObjectClassDefinition(name = "Meter Eastron SDM 120", // diff --git a/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm120/MeterEastronSdm120Impl.java b/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm120/MeterEastronSdm120Impl.java index 3667375bb8b..ba24998b1c5 100644 --- a/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm120/MeterEastronSdm120Impl.java +++ b/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm120/MeterEastronSdm120Impl.java @@ -21,6 +21,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -34,7 +35,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; import io.openems.edge.meter.api.SinglePhaseMeter; import io.openems.edge.timedata.api.Timedata; diff --git a/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm630/Config.java b/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm630/Config.java index 072294858f1..371d25772ee 100644 --- a/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm630/Config.java +++ b/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm630/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(name = "Meter Eastron SDM 630", // description = "Implements the Eastron SDM630 meter.") diff --git a/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm630/MeterEastronSdm630Impl.java b/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm630/MeterEastronSdm630Impl.java index b636a7f42da..0c215c37ed3 100644 --- a/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm630/MeterEastronSdm630Impl.java +++ b/io.openems.edge.meter.eastron/src/io/openems/edge/meter/eastron/sdm630/MeterEastronSdm630Impl.java @@ -22,6 +22,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -36,7 +37,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; diff --git a/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm120/MeterEastronSdm120ImplTest.java b/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm120/MeterEastronSdm120ImplTest.java index 169778c8b20..050d8ac26b9 100644 --- a/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm120/MeterEastronSdm120ImplTest.java +++ b/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm120/MeterEastronSdm120ImplTest.java @@ -1,28 +1,26 @@ package io.openems.edge.meter.eastron.sdm120; +import static io.openems.common.types.MeterType.GRID; +import static io.openems.edge.meter.api.SinglePhase.L1; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; -import io.openems.edge.meter.api.SinglePhase; public class MeterEastronSdm120ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterEastronSdm120Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // - .setPhase(SinglePhase.L1) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // + .setPhase(L1) // .build()) // ; } diff --git a/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm120/MyConfig.java b/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm120/MyConfig.java index f1e83072c87..10cbce9a833 100644 --- a/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm120/MyConfig.java +++ b/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm120/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.eastron.sdm120; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; @SuppressWarnings("all") diff --git a/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm630/MeterEastronSdm630ImplTest.java b/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm630/MeterEastronSdm630ImplTest.java index d79ef87a631..e291fe7ebcd 100644 --- a/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm630/MeterEastronSdm630ImplTest.java +++ b/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm630/MeterEastronSdm630ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.eastron.sdm630; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterEastronSdm630ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterEastronSdm630Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm630/MyConfig.java b/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm630/MyConfig.java index d686ca017c8..ab2389f95ea 100644 --- a/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm630/MyConfig.java +++ b/io.openems.edge.meter.eastron/test/io/openems/edge/meter/eastron/sdm630/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.eastron.sdm630; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg511/Config.java b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg511/Config.java index 74cffce7ed0..bc652b3732e 100644 --- a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg511/Config.java +++ b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg511/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Janitza UMG 511", // diff --git a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg511/MeterJanitzaUmg511Impl.java b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg511/MeterJanitzaUmg511Impl.java index b4ad5712068..6e79361ce31 100644 --- a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg511/MeterJanitzaUmg511Impl.java +++ b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg511/MeterJanitzaUmg511Impl.java @@ -17,6 +17,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -29,7 +30,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; /** * Implements the Janitza UMG 511 power analyzer. diff --git a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg604/Config.java b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg604/Config.java index cc79c8d9742..ef53697d991 100644 --- a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg604/Config.java +++ b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg604/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Janitza UMG 604", // diff --git a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg604/MeterJanitzaUmg604Impl.java b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg604/MeterJanitzaUmg604Impl.java index 95fa44bd68f..d2ea696abb4 100644 --- a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg604/MeterJanitzaUmg604Impl.java +++ b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg604/MeterJanitzaUmg604Impl.java @@ -17,6 +17,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -29,7 +30,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; /** * Implements the Janitza UMG 604 power analyzer. diff --git a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg96rme/Config.java b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg96rme/Config.java index aad641796ec..e64b3dd8b6b 100644 --- a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg96rme/Config.java +++ b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg96rme/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Janitza UMG 96RM-E", // diff --git a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg96rme/MeterJanitzaUmg96rmeImpl.java b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg96rme/MeterJanitzaUmg96rmeImpl.java index 7b9f91291a4..5af9f905a30 100644 --- a/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg96rme/MeterJanitzaUmg96rmeImpl.java +++ b/io.openems.edge.meter.janitza/src/io/openems/edge/meter/janitza/umg96rme/MeterJanitzaUmg96rmeImpl.java @@ -17,6 +17,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -29,7 +30,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; /** * Implements the Janitza UMG 96RM-E power analyzer. diff --git a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg511/MeterJanitzaUmg511ImplTest.java b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg511/MeterJanitzaUmg511ImplTest.java index 852686d87d1..3cc1195ff82 100644 --- a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg511/MeterJanitzaUmg511ImplTest.java +++ b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg511/MeterJanitzaUmg511ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.janitza.umg511; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterJanitzaUmg511ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterJanitzaUmg511Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg511/MyConfig.java b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg511/MyConfig.java index ded5dd84e28..56ed0452b5b 100644 --- a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg511/MyConfig.java +++ b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg511/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.janitza.umg511; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg604/MeterJanitzaUmg604ImplTest.java b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg604/MeterJanitzaUmg604ImplTest.java index 22fe623eb16..f309c6be3d1 100644 --- a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg604/MeterJanitzaUmg604ImplTest.java +++ b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg604/MeterJanitzaUmg604ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.janitza.umg604; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterJanitzaUmg604ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterJanitzaUmg604Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg604/MyConfig.java b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg604/MyConfig.java index e1cc866681b..770e25f4663 100644 --- a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg604/MyConfig.java +++ b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg604/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.janitza.umg604; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg96rme/MeterJanitzaUmg96rmeImplTest.java b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg96rme/MeterJanitzaUmg96rmeImplTest.java index ea3f2c9ddc2..7a1c5ed8a1b 100644 --- a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg96rme/MeterJanitzaUmg96rmeImplTest.java +++ b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg96rme/MeterJanitzaUmg96rmeImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.janitza.umg96rme; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterJanitzaUmg96rmeImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterJanitzaUmg96rmeImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg96rme/MyConfig.java b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg96rme/MyConfig.java index 68e0d2c2008..5d68b21c458 100644 --- a/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg96rme/MyConfig.java +++ b/io.openems.edge.meter.janitza/test/io/openems/edge/meter/janitza/umg96rme/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.janitza.umg96rme; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.kdk/src/io/openems/edge/meter/kdk/puct2/Config.java b/io.openems.edge.meter.kdk/src/io/openems/edge/meter/kdk/puct2/Config.java index a9c3415de16..98b9fca80d5 100755 --- a/io.openems.edge.meter.kdk/src/io/openems/edge/meter/kdk/puct2/Config.java +++ b/io.openems.edge.meter.kdk/src/io/openems/edge/meter/kdk/puct2/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter KDK 2PU CT", // diff --git a/io.openems.edge.meter.kdk/src/io/openems/edge/meter/kdk/puct2/MeterKdk2puctImpl.java b/io.openems.edge.meter.kdk/src/io/openems/edge/meter/kdk/puct2/MeterKdk2puctImpl.java index a8e380226d3..f3f11b84e11 100755 --- a/io.openems.edge.meter.kdk/src/io/openems/edge/meter/kdk/puct2/MeterKdk2puctImpl.java +++ b/io.openems.edge.meter.kdk/src/io/openems/edge/meter/kdk/puct2/MeterKdk2puctImpl.java @@ -19,6 +19,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -36,7 +37,6 @@ import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.common.type.TypeUtils; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.kdk/test/io/openems/edge/meter/kdk/puct2/MeterKdk2puctImplTest.java b/io.openems.edge.meter.kdk/test/io/openems/edge/meter/kdk/puct2/MeterKdk2puctImplTest.java index 8cc7f90bdc4..f042c8e572d 100755 --- a/io.openems.edge.meter.kdk/test/io/openems/edge/meter/kdk/puct2/MeterKdk2puctImplTest.java +++ b/io.openems.edge.meter.kdk/test/io/openems/edge/meter/kdk/puct2/MeterKdk2puctImplTest.java @@ -1,28 +1,26 @@ package io.openems.edge.meter.kdk.puct2; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterKdk2puctImplTest { - private static final String COMPONENT_ID = "component0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterKdk2puctImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setModbusId(MODBUS_ID) // + .setId("component0") // + .setModbusId("modbus0") // .setModbusUnitId(1) // - .setMeterType(MeterType.GRID) // + .setMeterType(GRID) // .setInvert(false) // .build()) .next(new TestCase()); diff --git a/io.openems.edge.meter.kdk/test/io/openems/edge/meter/kdk/puct2/MyConfig.java b/io.openems.edge.meter.kdk/test/io/openems/edge/meter/kdk/puct2/MyConfig.java index 1271760edf0..943b1f5c06f 100755 --- a/io.openems.edge.meter.kdk/test/io/openems/edge/meter/kdk/puct2/MyConfig.java +++ b/io.openems.edge.meter.kdk/test/io/openems/edge/meter/kdk/puct2/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.kdk.puct2; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/Config.java b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/Config.java index 7797744593a..7d35f93e563 100644 --- a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/Config.java +++ b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Phoenix Contact", // diff --git a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImpl.java b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImpl.java index 146005b63ff..37b66d4356c 100644 --- a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImpl.java +++ b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImpl.java @@ -18,6 +18,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -31,7 +32,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/MyConfig.java b/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/MyConfig.java index f168618fdee..d59d6e3d362 100644 --- a/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/MyConfig.java +++ b/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.phoenixcontact; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImplTest.java b/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImplTest.java index c6801784fb8..38cbacc4542 100644 --- a/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImplTest.java +++ b/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImplTest.java @@ -1,27 +1,25 @@ package io.openems.edge.meter.phoenixcontact; +import static io.openems.common.types.MeterType.PRODUCTION; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class PhoenixContactMeterImplTest { - private static final String COMPONENT_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new PhoenixContactMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setModbusId(MODBUS_ID) // - .setMeterType(MeterType.PRODUCTION) // + .setId("meter0") // + .setModbusId("modbus0") // + .setMeterType(PRODUCTION) // .build()) .next(new TestCase()); } diff --git a/io.openems.edge.meter.plexlog/src/io/openems/edge/meter/plexlog/Config.java b/io.openems.edge.meter.plexlog/src/io/openems/edge/meter/plexlog/Config.java index bd9e1b71ee4..fe27dbe8ab0 100644 --- a/io.openems.edge.meter.plexlog/src/io/openems/edge/meter/plexlog/Config.java +++ b/io.openems.edge.meter.plexlog/src/io/openems/edge/meter/plexlog/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Plexlog Datalogger", // diff --git a/io.openems.edge.meter.plexlog/src/io/openems/edge/meter/plexlog/MeterPlexlogDataloggerImpl.java b/io.openems.edge.meter.plexlog/src/io/openems/edge/meter/plexlog/MeterPlexlogDataloggerImpl.java index 50bf5fa08f8..f18568c5ec9 100644 --- a/io.openems.edge.meter.plexlog/src/io/openems/edge/meter/plexlog/MeterPlexlogDataloggerImpl.java +++ b/io.openems.edge.meter.plexlog/src/io/openems/edge/meter/plexlog/MeterPlexlogDataloggerImpl.java @@ -15,6 +15,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -27,7 +28,6 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.plexlog/test/io/openems/edge/meter/plexlog/MeterPlexlogDataloggerImplTest.java b/io.openems.edge.meter.plexlog/test/io/openems/edge/meter/plexlog/MeterPlexlogDataloggerImplTest.java index b5d53bd93e7..57ce87bcbe0 100644 --- a/io.openems.edge.meter.plexlog/test/io/openems/edge/meter/plexlog/MeterPlexlogDataloggerImplTest.java +++ b/io.openems.edge.meter.plexlog/test/io/openems/edge/meter/plexlog/MeterPlexlogDataloggerImplTest.java @@ -1,27 +1,25 @@ package io.openems.edge.meter.plexlog; +import static io.openems.common.types.MeterType.PRODUCTION; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterPlexlogDataloggerImplTest { - private static final String COMPONENT_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterPlexlogDataloggerImpl()) // .addReference("cm", new DummyConfigurationAdmin()) - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setMeterType(MeterType.PRODUCTION) // - .setModbusId(MODBUS_ID) // + .setId("meter0") // + .setMeterType(PRODUCTION) // + .setModbusId("modbus0") // .build()) .next(new TestCase()); } diff --git a/io.openems.edge.meter.plexlog/test/io/openems/edge/meter/plexlog/MyConfig.java b/io.openems.edge.meter.plexlog/test/io/openems/edge/meter/plexlog/MyConfig.java index 33027ba6d0f..1de26eb6930 100644 --- a/io.openems.edge.meter.plexlog/test/io/openems/edge/meter/plexlog/MyConfig.java +++ b/io.openems.edge.meter.plexlog/test/io/openems/edge/meter/plexlog/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.plexlog; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/Config.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/Config.java index ee9703714c0..ce230fe69b4 100644 --- a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/Config.java +++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter PQ-Plus UMD 96", // diff --git a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Impl.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Impl.java index 248c2c30784..6c6202f7841 100644 --- a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Impl.java +++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Impl.java @@ -15,6 +15,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -25,7 +26,6 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; /** * Implements the PQ Plus UMD 96 meter. diff --git a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/Config.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/Config.java index 6f158db2353..685b59a10bc 100644 --- a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/Config.java +++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter PQ-Plus UMD 97", // diff --git a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Impl.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Impl.java index 72accb117fb..e77f309fd9a 100644 --- a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Impl.java +++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Impl.java @@ -15,6 +15,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -25,7 +26,6 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; /** * Implements the PQ Plus UMD 97 meter. diff --git a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96ImplTest.java b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96ImplTest.java index 403893ed299..8599768a21d 100644 --- a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96ImplTest.java +++ b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.pqplus.umd96; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterPqplusUmd96ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterPqplusUmd96Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MyConfig.java b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MyConfig.java index bc502d3e9f2..2882de0ec27 100644 --- a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MyConfig.java +++ b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.pqplus.umd96; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97ImplTest.java b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97ImplTest.java index 106baa6ae23..2f7c436e66d 100644 --- a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97ImplTest.java +++ b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.pqplus.umd97; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterPqplusUmd97ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterPqplusUmd97Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java index a924208764e..d906352df16 100644 --- a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java +++ b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.pqplus.umd97; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.schneider.acti9.smartlink/src/io/openems/edge/meter/schneider/acti9/smartlink/Config.java b/io.openems.edge.meter.schneider.acti9.smartlink/src/io/openems/edge/meter/schneider/acti9/smartlink/Config.java index eff440fdddb..b61d6586f8b 100644 --- a/io.openems.edge.meter.schneider.acti9.smartlink/src/io/openems/edge/meter/schneider/acti9/smartlink/Config.java +++ b/io.openems.edge.meter.schneider.acti9.smartlink/src/io/openems/edge/meter/schneider/acti9/smartlink/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Schneider Acti9 Smartlink", // diff --git a/io.openems.edge.meter.schneider.acti9.smartlink/src/io/openems/edge/meter/schneider/acti9/smartlink/MeterSchneiderActi9SmartlinkImpl.java b/io.openems.edge.meter.schneider.acti9.smartlink/src/io/openems/edge/meter/schneider/acti9/smartlink/MeterSchneiderActi9SmartlinkImpl.java index f2d9c28e77c..5c3419cbe6f 100644 --- a/io.openems.edge.meter.schneider.acti9.smartlink/src/io/openems/edge/meter/schneider/acti9/smartlink/MeterSchneiderActi9SmartlinkImpl.java +++ b/io.openems.edge.meter.schneider.acti9.smartlink/src/io/openems/edge/meter/schneider/acti9/smartlink/MeterSchneiderActi9SmartlinkImpl.java @@ -16,6 +16,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -27,7 +28,6 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.schneider.acti9.smartlink/test/io/openems/edge/meter/schneider/acti9/smartlink/MeterSchneiderActi9SmartlinkImplTest.java b/io.openems.edge.meter.schneider.acti9.smartlink/test/io/openems/edge/meter/schneider/acti9/smartlink/MeterSchneiderActi9SmartlinkImplTest.java index 31b7ac51617..1cb4e0a5112 100644 --- a/io.openems.edge.meter.schneider.acti9.smartlink/test/io/openems/edge/meter/schneider/acti9/smartlink/MeterSchneiderActi9SmartlinkImplTest.java +++ b/io.openems.edge.meter.schneider.acti9.smartlink/test/io/openems/edge/meter/schneider/acti9/smartlink/MeterSchneiderActi9SmartlinkImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.schneider.acti9.smartlink; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterSchneiderActi9SmartlinkImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterSchneiderActi9SmartlinkImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .setInvert(false) // .build()) // ; diff --git a/io.openems.edge.meter.schneider.acti9.smartlink/test/io/openems/edge/meter/schneider/acti9/smartlink/MyConfig.java b/io.openems.edge.meter.schneider.acti9.smartlink/test/io/openems/edge/meter/schneider/acti9/smartlink/MyConfig.java index 6be2798bdbd..72fee02b36c 100644 --- a/io.openems.edge.meter.schneider.acti9.smartlink/test/io/openems/edge/meter/schneider/acti9/smartlink/MyConfig.java +++ b/io.openems.edge.meter.schneider.acti9.smartlink/test/io/openems/edge/meter/schneider/acti9/smartlink/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.schneider.acti9.smartlink; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac1600/Config.java b/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac1600/Config.java index 0143777c75b..8b3b7e76c0c 100644 --- a/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac1600/Config.java +++ b/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac1600/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Siemens PAC1600", // diff --git a/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac1600/MeterSiemensPac1600Impl.java b/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac1600/MeterSiemensPac1600Impl.java index dbe1c463ce1..8adfba56bce 100644 --- a/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac1600/MeterSiemensPac1600Impl.java +++ b/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac1600/MeterSiemensPac1600Impl.java @@ -13,6 +13,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; @@ -27,7 +28,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac2200/Config.java b/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac2200/Config.java index c100374f8d1..1033011652e 100644 --- a/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac2200/Config.java +++ b/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac2200/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Siemens PAC2200/PAC3200/PAC4200", // diff --git a/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac2200/MeterSiemensPac2200Impl.java b/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac2200/MeterSiemensPac2200Impl.java index 3c2e8f5d7fa..a70b44e5e59 100644 --- a/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac2200/MeterSiemensPac2200Impl.java +++ b/io.openems.edge.meter.siemens/src/io/openems/edge/meter/siemens/pac2200/MeterSiemensPac2200Impl.java @@ -17,6 +17,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -30,7 +31,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; /** * Implements the Siemens PAC2200/PAC3200/PAC4200 power meter. diff --git a/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac1600/MeterSiemensPac1600ImplTest.java b/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac1600/MeterSiemensPac1600ImplTest.java index 98707507163..51a3d0b5c7f 100644 --- a/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac1600/MeterSiemensPac1600ImplTest.java +++ b/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac1600/MeterSiemensPac1600ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.siemens.pac1600; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterSiemensPac1600ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterSiemensPac1600Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac1600/MyConfig.java b/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac1600/MyConfig.java index 52cc1aa53da..c969615d9fc 100644 --- a/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac1600/MyConfig.java +++ b/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac1600/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.siemens.pac1600; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac2200/MeterSiemensPac2200ImplTest.java b/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac2200/MeterSiemensPac2200ImplTest.java index 51a0989cde4..a75b0c69dce 100644 --- a/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac2200/MeterSiemensPac2200ImplTest.java +++ b/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac2200/MeterSiemensPac2200ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.siemens.pac2200; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterSiemensPac2200ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterSiemensPac2200Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .setInvert(false) // .build()) // ; diff --git a/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac2200/MyConfig.java b/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac2200/MyConfig.java index e652b511f87..be3116bd633 100644 --- a/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac2200/MyConfig.java +++ b/io.openems.edge.meter.siemens/test/io/openems/edge/meter/siemens/pac2200/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.siemens.pac2200; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.sma.shm20/src/io/openems/edge/meter/sma/shm20/Config.java b/io.openems.edge.meter.sma.shm20/src/io/openems/edge/meter/sma/shm20/Config.java index 9c477ae2878..8564caf5a70 100644 --- a/io.openems.edge.meter.sma.shm20/src/io/openems/edge/meter/sma/shm20/Config.java +++ b/io.openems.edge.meter.sma.shm20/src/io/openems/edge/meter/sma/shm20/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(name = "Meter SMA Sunny Home Manager 2.0", // description = "Implements the SMA Sunny Home Manager 2.0 integrated meter.") diff --git a/io.openems.edge.meter.sma.shm20/src/io/openems/edge/meter/sma/shm20/MeterSmaShm20Impl.java b/io.openems.edge.meter.sma.shm20/src/io/openems/edge/meter/sma/shm20/MeterSmaShm20Impl.java index 7e31503d6e9..9fb292f460f 100644 --- a/io.openems.edge.meter.sma.shm20/src/io/openems/edge/meter/sma/shm20/MeterSmaShm20Impl.java +++ b/io.openems.edge.meter.sma.shm20/src/io/openems/edge/meter/sma/shm20/MeterSmaShm20Impl.java @@ -17,6 +17,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -29,7 +30,6 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.sma.shm20/test/io/openems/edge/meter/sma/shm20/MeterSmaShm20ImplTest.java b/io.openems.edge.meter.sma.shm20/test/io/openems/edge/meter/sma/shm20/MeterSmaShm20ImplTest.java index 2d72d2aa8ee..3afb34d95ad 100644 --- a/io.openems.edge.meter.sma.shm20/test/io/openems/edge/meter/sma/shm20/MeterSmaShm20ImplTest.java +++ b/io.openems.edge.meter.sma.shm20/test/io/openems/edge/meter/sma/shm20/MeterSmaShm20ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.sma.shm20; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterSmaShm20ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterSmaShm20Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.sma.shm20/test/io/openems/edge/meter/sma/shm20/MyConfig.java b/io.openems.edge.meter.sma.shm20/test/io/openems/edge/meter/sma/shm20/MyConfig.java index 97a925d683c..442a28eacfd 100644 --- a/io.openems.edge.meter.sma.shm20/test/io/openems/edge/meter/sma/shm20/MyConfig.java +++ b/io.openems.edge.meter.sma.shm20/test/io/openems/edge/meter/sma/shm20/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.sma.shm20; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/singlephase/Config.java b/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/singlephase/Config.java index e7f2f875d2e..b8eb095a2f3 100644 --- a/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/singlephase/Config.java +++ b/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/singlephase/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; import io.openems.edge.meter.api.SinglePhase; @ObjectClassDefinition(// diff --git a/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/singlephase/MeterSocomecSinglephaseImpl.java b/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/singlephase/MeterSocomecSinglephaseImpl.java index 04317f76ce8..5db8dbdd32a 100644 --- a/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/singlephase/MeterSocomecSinglephaseImpl.java +++ b/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/singlephase/MeterSocomecSinglephaseImpl.java @@ -21,6 +21,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement; @@ -32,7 +33,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; import io.openems.edge.meter.api.SinglePhaseMeter; import io.openems.edge.meter.socomec.AbstractSocomecMeter; diff --git a/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/threephase/Config.java b/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/threephase/Config.java index e5188cf84c6..8cf48d76e88 100644 --- a/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/threephase/Config.java +++ b/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/threephase/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Socomec Threephase", // diff --git a/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/threephase/MeterSocomecThreephaseImpl.java b/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/threephase/MeterSocomecThreephaseImpl.java index 5152fbde0a0..9e82f5e5132 100644 --- a/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/threephase/MeterSocomecThreephaseImpl.java +++ b/io.openems.edge.meter.socomec/src/io/openems/edge/meter/socomec/threephase/MeterSocomecThreephaseImpl.java @@ -21,6 +21,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement; @@ -32,7 +33,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.socomec.AbstractSocomecMeter; import io.openems.edge.meter.socomec.SocomecMeter; diff --git a/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/singlephase/MeterSocomecSinglephaseImplTest.java b/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/singlephase/MeterSocomecSinglephaseImplTest.java index 7b232645b1f..9565b4bb274 100644 --- a/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/singlephase/MeterSocomecSinglephaseImplTest.java +++ b/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/singlephase/MeterSocomecSinglephaseImplTest.java @@ -1,19 +1,17 @@ package io.openems.edge.meter.socomec.singlephase; +import static io.openems.common.types.MeterType.GRID; +import static io.openems.edge.meter.api.SinglePhase.L1; + import org.junit.Before; import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; -import io.openems.edge.meter.api.SinglePhase; public class MeterSocomecSinglephaseImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - private static MeterSocomecSinglephaseImpl meter; @Before @@ -21,13 +19,13 @@ public void setup() throws Exception { meter = new MeterSocomecSinglephaseImpl(); new ComponentTest(meter) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .setInvert(false) // - .setPhase(SinglePhase.L1) // + .setPhase(L1) // .build()); // } diff --git a/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/singlephase/MyConfig.java b/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/singlephase/MyConfig.java index d17afa4603c..23f79f2a771 100644 --- a/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/singlephase/MyConfig.java +++ b/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/singlephase/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.socomec.singlephase; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; @SuppressWarnings("all") diff --git a/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/threephase/MeterSocomecThreephaseImplTest.java b/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/threephase/MeterSocomecThreephaseImplTest.java index 399894b186f..5d3ff68ab51 100644 --- a/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/threephase/MeterSocomecThreephaseImplTest.java +++ b/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/threephase/MeterSocomecThreephaseImplTest.java @@ -1,18 +1,16 @@ package io.openems.edge.meter.socomec.threephase; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Before; import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterSocomecThreephaseImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - private static MeterSocomecThreephaseImpl meter; @Before @@ -20,11 +18,11 @@ public void setup() throws Exception { meter = new MeterSocomecThreephaseImpl(); new ComponentTest(meter) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .setInvert(false) // .build()); // } diff --git a/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/threephase/MyConfig.java b/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/threephase/MyConfig.java index 23923e567d1..1aaef059a46 100644 --- a/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/threephase/MyConfig.java +++ b/io.openems.edge.meter.socomec/test/io/openems/edge/meter/socomec/threephase/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.socomec.threephase; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/add/Config.java b/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/add/Config.java index c36a906d9b8..4fc95f884ca 100644 --- a/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/add/Config.java +++ b/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/add/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Virtual Add", // diff --git a/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/add/MeterVirtualAddImpl.java b/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/add/MeterVirtualAddImpl.java index b31e3c66280..6f325670232 100644 --- a/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/add/MeterVirtualAddImpl.java +++ b/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/add/MeterVirtualAddImpl.java @@ -17,12 +17,12 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.MeterType; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.modbusslave.ModbusSlave; import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.VirtualMeter; @Designate(ocd = Config.class, factory = true) diff --git a/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/subtract/Config.java b/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/subtract/Config.java index 7ece79b26e9..4366e8af241 100644 --- a/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/subtract/Config.java +++ b/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/subtract/Config.java @@ -4,7 +4,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Virtual Subtract", // diff --git a/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/subtract/VirtualSubtractMeterImpl.java b/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/subtract/VirtualSubtractMeterImpl.java index 0f11e68eb9b..31e60176923 100644 --- a/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/subtract/VirtualSubtractMeterImpl.java +++ b/io.openems.edge.meter.virtual/src/io/openems/edge/meter/virtual/subtract/VirtualSubtractMeterImpl.java @@ -16,12 +16,12 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.MeterType; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.modbusslave.ModbusSlave; import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.VirtualMeter; @Designate(ocd = Config.class, factory = true) diff --git a/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/add/MeterVirtualAddImplTest.java b/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/add/MeterVirtualAddImplTest.java index 69dd6d1c616..9ca132e2e74 100644 --- a/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/add/MeterVirtualAddImplTest.java +++ b/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/add/MeterVirtualAddImplTest.java @@ -1,89 +1,59 @@ package io.openems.edge.meter.virtual.add; +import static io.openems.common.types.MeterType.GRID; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L1; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L2; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.ACTIVE_POWER_L3; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.FREQUENCY; +import static io.openems.edge.meter.api.ElectricityMeter.ChannelId.VOLTAGE; + import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.test.DummyElectricityMeter; public class MeterVirtualAddImplTest { - private static final String METER_ID = "meter0"; - private static final ChannelAddress METER_POWER = new ChannelAddress(METER_ID, "ActivePower"); - private static final ChannelAddress METER_VOLTAGE = new ChannelAddress(METER_ID, "Voltage"); - private static final ChannelAddress METER_FREQ = new ChannelAddress(METER_ID, "Frequency"); - - private static final ChannelAddress METER_POWER_L1 = new ChannelAddress(METER_ID, "ActivePowerL1"); - private static final ChannelAddress METER_POWER_L2 = new ChannelAddress(METER_ID, "ActivePowerL2"); - private static final ChannelAddress METER_POWER_L3 = new ChannelAddress(METER_ID, "ActivePowerL3"); - - private static final String METER_ID_1 = "meter1"; - private static final ChannelAddress METER_ID_1_ACTIVEPOWER = new ChannelAddress(METER_ID_1, "ActivePower"); - private static final ChannelAddress METER_ID_1_VOLTAGE = new ChannelAddress(METER_ID_1, "Voltage"); - private static final ChannelAddress METER_ID_1_FREQUENCY = new ChannelAddress(METER_ID_1, "Frequency"); - - private static final ChannelAddress METER_ID_1_ACTIVEPOWER_L1 = new ChannelAddress(METER_ID_1, "ActivePowerL1"); - private static final ChannelAddress METER_ID_1_ACTIVEPOWER_L2 = new ChannelAddress(METER_ID_1, "ActivePowerL2"); - private static final ChannelAddress METER_ID_1_ACTIVEPOWER_L3 = new ChannelAddress(METER_ID_1, "ActivePowerL3"); - - private static final String METER_ID_2 = "meter2"; - private static final ChannelAddress METER_ID_2_ACTIVEPOWER = new ChannelAddress(METER_ID_2, "ActivePower"); - private static final ChannelAddress METER_ID_2_VOLTAGE = new ChannelAddress(METER_ID_2, "Voltage"); - private static final ChannelAddress METER_ID_2_FREQUENCY = new ChannelAddress(METER_ID_2, "Frequency"); - - private static final ChannelAddress METER_ID_2_ACTIVEPOWER_L1 = new ChannelAddress(METER_ID_2, "ActivePowerL1"); - private static final ChannelAddress METER_ID_2_ACTIVEPOWER_L2 = new ChannelAddress(METER_ID_2, "ActivePowerL2"); - private static final ChannelAddress METER_ID_2_ACTIVEPOWER_L3 = new ChannelAddress(METER_ID_2, "ActivePowerL3"); - - private static final String METER_ID_3 = "meter3"; - private static final ChannelAddress METER_ID_3_ACTIVEPOWER = new ChannelAddress(METER_ID_3, "ActivePower"); - private static final ChannelAddress METER_ID_3_VOLTAGE = new ChannelAddress(METER_ID_3, "Voltage"); - private static final ChannelAddress METER_ID_3_FREQUENCY = new ChannelAddress(METER_ID_3, "Frequency"); - - private static final ChannelAddress METER_ID_3_ACTIVEPOWER_L1 = new ChannelAddress(METER_ID_3, "ActivePowerL1"); - private static final ChannelAddress METER_ID_3_ACTIVEPOWER_L2 = new ChannelAddress(METER_ID_3, "ActivePowerL2"); - private static final ChannelAddress METER_ID_3_ACTIVEPOWER_L3 = new ChannelAddress(METER_ID_3, "ActivePowerL3"); - @Test public void test() throws Exception { new ComponentTest(new MeterVirtualAddImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("addMeter", new DummyElectricityMeter(METER_ID_1)) - .addReference("addMeter", new DummyElectricityMeter(METER_ID_2)) // - .addReference("addMeter", new DummyElectricityMeter(METER_ID_3)) // + .addReference("addMeter", new DummyElectricityMeter("meter1")) + .addReference("addMeter", new DummyElectricityMeter("meter2")) // + .addReference("addMeter", new DummyElectricityMeter("meter3")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setMeterIds(METER_ID_1, METER_ID_2, METER_ID_3) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setMeterIds("meter1", "meter2", "meter3") // + .setType(GRID) // .build()) .next(new TestCase("one") // - .input(METER_ID_1_ACTIVEPOWER, 6_000) // - .input(METER_ID_1_ACTIVEPOWER_L1, 2_000) // - .input(METER_ID_1_ACTIVEPOWER_L2, 2_000) // - .input(METER_ID_1_ACTIVEPOWER_L3, 2_000) // - .input(METER_ID_2_ACTIVEPOWER, 7_500) // - .input(METER_ID_2_ACTIVEPOWER_L1, 2_500) // - .input(METER_ID_2_ACTIVEPOWER_L2, 2_500) // - .input(METER_ID_2_ACTIVEPOWER_L3, 2_500) // - .input(METER_ID_3_ACTIVEPOWER, 9_000) // - .input(METER_ID_3_ACTIVEPOWER_L1, 3_000) // - .input(METER_ID_3_ACTIVEPOWER_L2, 3_000) // - .input(METER_ID_3_ACTIVEPOWER_L3, 3_000) // - .input(METER_ID_1_VOLTAGE, 10) // - .input(METER_ID_2_VOLTAGE, 20) // - .input(METER_ID_3_VOLTAGE, 30) // - .input(METER_ID_1_FREQUENCY, 49) // - .input(METER_ID_2_FREQUENCY, 51) // - .input(METER_ID_3_FREQUENCY, 56)) // + .input("meter1", ACTIVE_POWER, 6_000) // + .input("meter1", ACTIVE_POWER_L1, 2_000) // + .input("meter1", ACTIVE_POWER_L2, 2_000) // + .input("meter1", ACTIVE_POWER_L3, 2_000) // + .input("meter2", ACTIVE_POWER, 7_500) // + .input("meter2", ACTIVE_POWER_L1, 2_500) // + .input("meter2", ACTIVE_POWER_L2, 2_500) // + .input("meter2", ACTIVE_POWER_L3, 2_500) // + .input("meter3", ACTIVE_POWER, 9_000) // + .input("meter3", ACTIVE_POWER_L1, 3_000) // + .input("meter3", ACTIVE_POWER_L2, 3_000) // + .input("meter3", ACTIVE_POWER_L3, 3_000) // + .input("meter1", VOLTAGE, 10) // + .input("meter2", VOLTAGE, 20) // + .input("meter3", VOLTAGE, 30) // + .input("meter1", FREQUENCY, 49) // + .input("meter2", FREQUENCY, 51) // + .input("meter3", FREQUENCY, 56)) // .next(new TestCase("two") // - .output(METER_POWER, 22_500) // - .output(METER_POWER_L1, 7_500) // - .output(METER_POWER_L2, 7_500) // - .output(METER_POWER_L3, 7_500) // - .output(METER_VOLTAGE, 20) // - .output(METER_FREQ, 52)); + .output(ACTIVE_POWER, 22_500) // + .output(ACTIVE_POWER_L1, 7_500) // + .output(ACTIVE_POWER_L2, 7_500) // + .output(ACTIVE_POWER_L3, 7_500) // + .output(VOLTAGE, 20) // + .output(FREQUENCY, 52)); } } diff --git a/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/add/MyConfig.java b/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/add/MyConfig.java index d51dda118d9..8e38a4efcba 100644 --- a/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/add/MyConfig.java +++ b/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/add/MyConfig.java @@ -1,7 +1,7 @@ package io.openems.edge.meter.virtual.add; import io.openems.common.test.AbstractComponentConfig; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/subtract/MyConfig.java b/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/subtract/MyConfig.java index 10eca8d640c..2dfa372ebf0 100644 --- a/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/subtract/MyConfig.java +++ b/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/subtract/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.virtual.subtract; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/subtract/VirtualSubtractMeterImplTest.java b/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/subtract/VirtualSubtractMeterImplTest.java index d92470e3068..a5c4772190a 100644 --- a/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/subtract/VirtualSubtractMeterImplTest.java +++ b/io.openems.edge.meter.virtual/test/io/openems/edge/meter/virtual/subtract/VirtualSubtractMeterImplTest.java @@ -1,60 +1,50 @@ package io.openems.edge.meter.virtual.subtract; -import org.junit.Test; +import static io.openems.common.types.MeterType.GRID; + +import java.util.List; -import com.google.common.collect.Lists; +import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; -import io.openems.edge.meter.api.MeterType; +import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.test.DummyElectricityMeter; public class VirtualSubtractMeterImplTest { - private static final String METER_ID = "meter0"; - private static final ChannelAddress METER_POWER = new ChannelAddress(METER_ID, "ActivePower"); - - private static final String MINUEND_ID = "meter1"; - private static final ChannelAddress MINUEND_POWER = new ChannelAddress(MINUEND_ID, "ActivePower"); - - private static final String SUBTRAHEND1_ID = "meter2"; - private static final ChannelAddress SUBTRAHEND1_POWER = new ChannelAddress(SUBTRAHEND1_ID, "ActivePower"); - - private static final String SUBTRAHEND2_ID = "ess0"; - private static final ChannelAddress SUBTRAHEND2_POWER = new ChannelAddress(SUBTRAHEND2_ID, "ActivePower"); - @Test public void test() throws Exception { new ComponentTest(new VirtualSubtractMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("minuend", new DummyElectricityMeter(MINUEND_ID)) // - .addReference("subtrahends", Lists.newArrayList(// - new DummyElectricityMeter(SUBTRAHEND1_ID), // - new DummyManagedSymmetricEss(SUBTRAHEND2_ID))) // + .addReference("minuend", new DummyElectricityMeter("meter1")) // + .addReference("subtrahends", List.of(// + new DummyElectricityMeter("meter2"), // + new DummyManagedSymmetricEss("ess0"))) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setType(GRID) // .setAddToSum(true) // - .setMinuendId(MINUEND_ID) // - .setSubtrahendsIds(SUBTRAHEND1_ID, SUBTRAHEND2_ID) // + .setMinuendId("meter1") // + .setSubtrahendsIds("meter2", "ess0") // .build()) // .next(new TestCase() // - .input(MINUEND_POWER, 5_000) // - .input(SUBTRAHEND1_POWER, 2_000) // - .input(SUBTRAHEND2_POWER, 4_000) // - .output(METER_POWER, -1000)) // + .input("meter1", ElectricityMeter.ChannelId.ACTIVE_POWER, 5_000) // + .input("meter2", ElectricityMeter.ChannelId.ACTIVE_POWER, 2_000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 4_000) // + .output("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, -1000)) // .next(new TestCase() // - .input(MINUEND_POWER, null) // - .input(SUBTRAHEND1_POWER, 2_000) // - .input(SUBTRAHEND2_POWER, 4_000) // - .output(METER_POWER, null)) // + .input("meter1", ElectricityMeter.ChannelId.ACTIVE_POWER, null) // + .input("meter2", ElectricityMeter.ChannelId.ACTIVE_POWER, 2_000) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 4_000) // + .output("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, null)) // .next(new TestCase() // - .input(MINUEND_POWER, 5_000) // - .input(SUBTRAHEND1_POWER, null) // - .input(SUBTRAHEND2_POWER, 4_000) // - .output(METER_POWER, 1000)); + .input("meter1", ElectricityMeter.ChannelId.ACTIVE_POWER, 5_000) // + .input("meter2", ElectricityMeter.ChannelId.ACTIVE_POWER, null) // + .input("ess0", SymmetricEss.ChannelId.ACTIVE_POWER, 4_000) // + .output("meter0", ElectricityMeter.ChannelId.ACTIVE_POWER, 1000)); } } \ No newline at end of file diff --git a/io.openems.edge.meter.weidmueller/src/io/openems/edge/meter/weidmueller/Config.java b/io.openems.edge.meter.weidmueller/src/io/openems/edge/meter/weidmueller/Config.java index 8f1c7107db9..6821bd4a123 100644 --- a/io.openems.edge.meter.weidmueller/src/io/openems/edge/meter/weidmueller/Config.java +++ b/io.openems.edge.meter.weidmueller/src/io/openems/edge/meter/weidmueller/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Weidmueller 525", // diff --git a/io.openems.edge.meter.weidmueller/src/io/openems/edge/meter/weidmueller/MeterWeidmueller525Impl.java b/io.openems.edge.meter.weidmueller/src/io/openems/edge/meter/weidmueller/MeterWeidmueller525Impl.java index 4dd38b5e0a3..245a476f35d 100644 --- a/io.openems.edge.meter.weidmueller/src/io/openems/edge/meter/weidmueller/MeterWeidmueller525Impl.java +++ b/io.openems.edge.meter.weidmueller/src/io/openems/edge/meter/weidmueller/MeterWeidmueller525Impl.java @@ -14,6 +14,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -25,7 +26,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.weidmueller/test/io/openems/edge/meter/weidmueller/MeterWeidmueller525ImplTest.java b/io.openems.edge.meter.weidmueller/test/io/openems/edge/meter/weidmueller/MeterWeidmueller525ImplTest.java index cb447a0c407..47f6a43085b 100644 --- a/io.openems.edge.meter.weidmueller/test/io/openems/edge/meter/weidmueller/MeterWeidmueller525ImplTest.java +++ b/io.openems.edge.meter.weidmueller/test/io/openems/edge/meter/weidmueller/MeterWeidmueller525ImplTest.java @@ -1,26 +1,24 @@ package io.openems.edge.meter.weidmueller; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterWeidmueller525ImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterWeidmueller525Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // - .setType(MeterType.GRID) // + .setId("meter0") // + .setModbusId("modbus0") // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.meter.weidmueller/test/io/openems/edge/meter/weidmueller/MyConfig.java b/io.openems.edge.meter.weidmueller/test/io/openems/edge/meter/weidmueller/MyConfig.java index cee040fdd54..78b45a398fc 100644 --- a/io.openems.edge.meter.weidmueller/test/io/openems/edge/meter/weidmueller/MyConfig.java +++ b/io.openems.edge.meter.weidmueller/test/io/openems/edge/meter/weidmueller/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.weidmueller; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.meter.ziehl/src/io/openems/edge/meter/ziehl/efr4001ip/Config.java b/io.openems.edge.meter.ziehl/src/io/openems/edge/meter/ziehl/efr4001ip/Config.java index 15188ec783f..81fae48a3dc 100644 --- a/io.openems.edge.meter.ziehl/src/io/openems/edge/meter/ziehl/efr4001ip/Config.java +++ b/io.openems.edge.meter.ziehl/src/io/openems/edge/meter/ziehl/efr4001ip/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(// name = "Meter Ziehl EFR4001IP", // diff --git a/io.openems.edge.meter.ziehl/src/io/openems/edge/meter/ziehl/efr4001ip/MeterZiehlEfr4001IpImpl.java b/io.openems.edge.meter.ziehl/src/io/openems/edge/meter/ziehl/efr4001ip/MeterZiehlEfr4001IpImpl.java index e6d852fcd0d..bfedc41e858 100644 --- a/io.openems.edge.meter.ziehl/src/io/openems/edge/meter/ziehl/efr4001ip/MeterZiehlEfr4001IpImpl.java +++ b/io.openems.edge.meter.ziehl/src/io/openems/edge/meter/ziehl/efr4001ip/MeterZiehlEfr4001IpImpl.java @@ -19,6 +19,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; @@ -32,7 +33,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.meter.ziehl/test/io/openems/edge/meter/ziehl/efr4001ip/MeterZiehlEfr4001IpImplTest.java b/io.openems.edge.meter.ziehl/test/io/openems/edge/meter/ziehl/efr4001ip/MeterZiehlEfr4001IpImplTest.java index 11397a94eee..7306a4697c7 100644 --- a/io.openems.edge.meter.ziehl/test/io/openems/edge/meter/ziehl/efr4001ip/MeterZiehlEfr4001IpImplTest.java +++ b/io.openems.edge.meter.ziehl/test/io/openems/edge/meter/ziehl/efr4001ip/MeterZiehlEfr4001IpImplTest.java @@ -1,28 +1,26 @@ package io.openems.edge.meter.ziehl.efr4001ip; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class MeterZiehlEfr4001IpImplTest { - private static final String COMPONENT_ID = "component0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new MeterZiehlEfr4001IpImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setModbusId(MODBUS_ID) // + .setId("component0") // + .setModbusId("modbus0") // .setModbusUnitId(1) // - .setMeterType(MeterType.GRID) // + .setMeterType(GRID) // .setInvert(false) // .build()) .next(new TestCase()); diff --git a/io.openems.edge.meter.ziehl/test/io/openems/edge/meter/ziehl/efr4001ip/MyConfig.java b/io.openems.edge.meter.ziehl/test/io/openems/edge/meter/ziehl/efr4001ip/MyConfig.java index c46214848ca..b5d0c49f635 100644 --- a/io.openems.edge.meter.ziehl/test/io/openems/edge/meter/ziehl/efr4001ip/MyConfig.java +++ b/io.openems.edge.meter.ziehl/test/io/openems/edge/meter/ziehl/efr4001ip/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.meter.ziehl.efr4001ip; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PredictorPersistenceModelImplTest.java b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PredictorPersistenceModelImplTest.java index 4cd7b472cdd..d0d95b109eb 100644 --- a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PredictorPersistenceModelImplTest.java +++ b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PredictorPersistenceModelImplTest.java @@ -1,34 +1,28 @@ package io.openems.edge.predictor.persistencemodel; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.predictor.api.prediction.LogVerbosity.NONE; import static io.openems.edge.predictor.api.prediction.Prediction.EMPTY_PREDICTION; +import static java.time.temporal.ChronoUnit.HOURS; import static org.junit.Assert.assertEquals; -import java.time.Instant; import java.time.ZoneId; -import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; import org.junit.Test; -import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.predictor.api.prediction.LogVerbosity; import io.openems.edge.timedata.test.DummyTimedata; public class PredictorPersistenceModelImplTest { - 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 var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); + final var clock = createDummyClock(); 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, @@ -43,7 +37,7 @@ public void test() throws Exception { 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 }; - var timedata = new DummyTimedata(TIMEDATA_ID); + var timedata = new DummyTimedata("timedata0"); var start = ZonedDateTime.of(2019, 12, 30, 0, 0, 0, 0, ZoneId.of("UTC")); for (var i = 0; i < values.length; i++) { timedata.add(start.plusMinutes(i * 15), METER1_ACTIVE_POWER, values[i]); @@ -55,9 +49,9 @@ public void test() throws Exception { .addReference("timedata", timedata) // .addReference("componentManager", new DummyComponentManager(clock)) // .activate(MyConfig.create() // - .setId(PREDICTOR_ID) // + .setId("predictor0") // .setChannelAddresses(METER1_ACTIVE_POWER.toString()) // - .setLogVerbosity(LogVerbosity.NONE) // + .setLogVerbosity(NONE) // .build()); var prediction = sut.getPrediction(METER1_ACTIVE_POWER); @@ -74,7 +68,7 @@ public void test() throws Exception { @Test public void test2() throws Exception { var start = ZonedDateTime.of(2019, 12, 30, 0, 0, 0, 0, ZoneId.of("UTC")); - final var clock = new TimeLeapClock(start.toInstant(), ZoneOffset.UTC); + final var clock = createDummyClock(); int[] values = { // Day 1 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 19, 74, 323, @@ -87,7 +81,7 @@ public void test2() throws Exception { 7320, 5950, 5644, 7157, 6847, 6549, 6498, 6296, 6096, 5895, 5658, 5372, 5011, 4603, 4159, 3831, 3400, 2757, 727, 194, 70, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - var timedata = new DummyTimedata(TIMEDATA_ID); + var timedata = new DummyTimedata("timedata0"); for (var i = 0; i < values.length; i++) { timedata.add(start.plusMinutes(i * 15), METER1_ACTIVE_POWER, values[i]); } @@ -98,30 +92,29 @@ public void test2() throws Exception { .addReference("timedata", timedata) // .addReference("componentManager", new DummyComponentManager(clock)) // .activate(MyConfig.create() // - .setId(PREDICTOR_ID) // + .setId("predictor0") // .setChannelAddresses(METER1_ACTIVE_POWER.toString()) // - .setLogVerbosity(LogVerbosity.NONE) // + .setLogVerbosity(NONE) // .build()); - clock.leap(39, ChronoUnit.HOURS); + clock.leap(39, HOURS); sut.getPrediction(METER1_ACTIVE_POWER); } @Test public void testEmpty() throws Exception { - final var clock = new TimeLeapClock(Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, - ZoneOffset.UTC); - var timedata = new DummyTimedata(TIMEDATA_ID); + final var clock = createDummyClock(); + var timedata = new DummyTimedata("timedata0"); var sut = new PredictorPersistenceModelImpl(); new ComponentTest(sut) // .addReference("timedata", timedata) // .addReference("componentManager", new DummyComponentManager(clock)) // .activate(MyConfig.create() // - .setId(PREDICTOR_ID) // + .setId("predictor0") // .setChannelAddresses(METER1_ACTIVE_POWER.toString()) // - .setLogVerbosity(LogVerbosity.NONE) // + .setLogVerbosity(NONE) // .build()); assertEquals(EMPTY_PREDICTION, sut.getPrediction(METER1_ACTIVE_POWER)); diff --git a/io.openems.edge.predictor.similardaymodel/test/io/openems/edge/predictor/similardaymodel/PredictorSimilardayModelImplTest.java b/io.openems.edge.predictor.similardaymodel/test/io/openems/edge/predictor/similardaymodel/PredictorSimilardayModelImplTest.java index 6374c6a2b55..8f9e3d25fd3 100644 --- a/io.openems.edge.predictor.similardaymodel/test/io/openems/edge/predictor/similardaymodel/PredictorSimilardayModelImplTest.java +++ b/io.openems.edge.predictor.similardaymodel/test/io/openems/edge/predictor/similardaymodel/PredictorSimilardayModelImplTest.java @@ -18,9 +18,6 @@ public class PredictorSimilardayModelImplTest { - 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 @@ -32,7 +29,7 @@ public void test() throws Exception { var values = Data.data; var predictedValues = Data.predictedData; - var timedata = new DummyTimedata(TIMEDATA_ID); + var timedata = new DummyTimedata("timedata0"); var start = ZonedDateTime.of(2019, 12, 1, 0, 0, 0, 0, ZoneId.of("UTC")); for (var i = 0; i < values.length; i++) { @@ -45,7 +42,7 @@ public void test() throws Exception { .addReference("timedata", timedata) // .addReference("componentManager", new DummyComponentManager(clock)) // .activate(MyConfig.create() // - .setId(PREDICTOR_ID) // + .setId("predictor0") // .setNumOfWeeks(4) // .setChannelAddresses(METER1_ACTIVE_POWER.toString()) // .setLogVerbosity(LogVerbosity.NONE) // diff --git a/io.openems.edge.pvinverter.api/src/io/openems/edge/pvinverter/api/ManagedSymmetricPvInverter.java b/io.openems.edge.pvinverter.api/src/io/openems/edge/pvinverter/api/ManagedSymmetricPvInverter.java index 0aec718a553..7db5a9dc100 100644 --- a/io.openems.edge.pvinverter.api/src/io/openems/edge/pvinverter/api/ManagedSymmetricPvInverter.java +++ b/io.openems.edge.pvinverter.api/src/io/openems/edge/pvinverter/api/ManagedSymmetricPvInverter.java @@ -4,6 +4,7 @@ import io.openems.common.channel.PersistencePriority; import io.openems.common.channel.Unit; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.MeterType; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.IntegerDoc; @@ -13,7 +14,6 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; /** * Represents a 3-Phase, symmetric PV-Inverter. diff --git a/io.openems.edge.pvinverter.cluster/test/io/openems/edge/pvinverter/cluster/PvInverterClusterImplTest.java b/io.openems.edge.pvinverter.cluster/test/io/openems/edge/pvinverter/cluster/PvInverterClusterImplTest.java index 8ea8a4efda1..c5d1d1c31a6 100644 --- a/io.openems.edge.pvinverter.cluster/test/io/openems/edge/pvinverter/cluster/PvInverterClusterImplTest.java +++ b/io.openems.edge.pvinverter.cluster/test/io/openems/edge/pvinverter/cluster/PvInverterClusterImplTest.java @@ -8,14 +8,12 @@ public class PvInverterClusterImplTest { - private static final String COMPONENT_ID = "io0"; - @Test public void test() throws Exception { new ComponentTest(new PvInverterClusterImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("io0") // .setPvInverterIds() // .build()) // .next(new TestCase()) // diff --git a/io.openems.edge.pvinverter.fronius/test/io/openems/edge/pvinverter/fronius/PvInverterFroniusImplTest.java b/io.openems.edge.pvinverter.fronius/test/io/openems/edge/pvinverter/fronius/PvInverterFroniusImplTest.java index 04e6f406c5f..db5e7b321d7 100644 --- a/io.openems.edge.pvinverter.fronius/test/io/openems/edge/pvinverter/fronius/PvInverterFroniusImplTest.java +++ b/io.openems.edge.pvinverter.fronius/test/io/openems/edge/pvinverter/fronius/PvInverterFroniusImplTest.java @@ -8,18 +8,15 @@ public class PvInverterFroniusImplTest { - private static final String PV_INVERTER_ID = "pvInverter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new PvInverterFroniusImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(PV_INVERTER_ID) // + .setId("pvInverter0") // .setReadOnly(true) // - .setModbusId(MODBUS_ID) // + .setModbusId("modbus0") // .setModbusUnitId(1) // .build()) // ; diff --git a/io.openems.edge.pvinverter.kaco.blueplanet/test/io/openems/edge/pvinverter/kaco/blueplanet/PvInverterKacoBlueplanetImplTest.java b/io.openems.edge.pvinverter.kaco.blueplanet/test/io/openems/edge/pvinverter/kaco/blueplanet/PvInverterKacoBlueplanetImplTest.java index 80811823b8e..af619a2deb4 100644 --- a/io.openems.edge.pvinverter.kaco.blueplanet/test/io/openems/edge/pvinverter/kaco/blueplanet/PvInverterKacoBlueplanetImplTest.java +++ b/io.openems.edge.pvinverter.kaco.blueplanet/test/io/openems/edge/pvinverter/kaco/blueplanet/PvInverterKacoBlueplanetImplTest.java @@ -8,18 +8,15 @@ public class PvInverterKacoBlueplanetImplTest { - private static final String PV_INVERTER_ID = "pvInverter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new PvInverterKacoBlueplanetImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(PV_INVERTER_ID) // + .setId("pvInverter0") // .setReadOnly(true) // - .setModbusId(MODBUS_ID) // + .setModbusId("modbus0") // .setModbusUnitId(1) // .build()) // ; diff --git a/io.openems.edge.pvinverter.kostal/test/io/openems/edge/pvinverter/kostal/PvInverterKostalImplTest.java b/io.openems.edge.pvinverter.kostal/test/io/openems/edge/pvinverter/kostal/PvInverterKostalImplTest.java index d9b74670758..c9f44649eb9 100644 --- a/io.openems.edge.pvinverter.kostal/test/io/openems/edge/pvinverter/kostal/PvInverterKostalImplTest.java +++ b/io.openems.edge.pvinverter.kostal/test/io/openems/edge/pvinverter/kostal/PvInverterKostalImplTest.java @@ -8,18 +8,15 @@ public class PvInverterKostalImplTest { - private static final String PV_INVERTER_ID = "pvInverter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new PvInverterKostalImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(PV_INVERTER_ID) // + .setId("pvInverter0") // .setReadOnly(true) // - .setModbusId(MODBUS_ID) // + .setModbusId("modbus0") // .setModbusUnitId(1) // .build()) // ; diff --git a/io.openems.edge.pvinverter.sma/test/io/openems/edge/pvinverter/sma/PvInverterSmaSunnyTripowerImplTest.java b/io.openems.edge.pvinverter.sma/test/io/openems/edge/pvinverter/sma/PvInverterSmaSunnyTripowerImplTest.java index 27b7a2d9d03..6e1b94cf7b0 100644 --- a/io.openems.edge.pvinverter.sma/test/io/openems/edge/pvinverter/sma/PvInverterSmaSunnyTripowerImplTest.java +++ b/io.openems.edge.pvinverter.sma/test/io/openems/edge/pvinverter/sma/PvInverterSmaSunnyTripowerImplTest.java @@ -1,28 +1,26 @@ package io.openems.edge.pvinverter.sma; +import static io.openems.edge.pvinverter.sunspec.Phase.ALL; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.pvinverter.sunspec.Phase; public class PvInverterSmaSunnyTripowerImplTest { - private static final String PV_INVERTER_ID = "pvInverter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new PvInverterSmaSunnyTripowerImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(PV_INVERTER_ID) // + .setId("pvInverter0") // .setReadOnly(true) // - .setModbusId(MODBUS_ID) // + .setModbusId("modbus0") // .setModbusUnitId(1) // - .setPhase(Phase.ALL) // + .setPhase(ALL) // .build()) // ; } diff --git a/io.openems.edge.pvinverter.solarlog/src/io/openems/edge/pvinverter/solarlog/PvInverterSolarlogImpl.java b/io.openems.edge.pvinverter.solarlog/src/io/openems/edge/pvinverter/solarlog/PvInverterSolarlogImpl.java index cf8c62c0da1..31992fe3563 100644 --- a/io.openems.edge.pvinverter.solarlog/src/io/openems/edge/pvinverter/solarlog/PvInverterSolarlogImpl.java +++ b/io.openems.edge.pvinverter.solarlog/src/io/openems/edge/pvinverter/solarlog/PvInverterSolarlogImpl.java @@ -22,6 +22,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ChannelMetaInfoReadAndWrite; @@ -41,7 +42,6 @@ import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; diff --git a/io.openems.edge.pvinverter.solarlog/test/io/openems/edge/pvinverter/solarlog/PvInverterSolarlogImplTest.java b/io.openems.edge.pvinverter.solarlog/test/io/openems/edge/pvinverter/solarlog/PvInverterSolarlogImplTest.java index 7d1f87a7b69..778f9f85f63 100644 --- a/io.openems.edge.pvinverter.solarlog/test/io/openems/edge/pvinverter/solarlog/PvInverterSolarlogImplTest.java +++ b/io.openems.edge.pvinverter.solarlog/test/io/openems/edge/pvinverter/solarlog/PvInverterSolarlogImplTest.java @@ -8,17 +8,14 @@ public class PvInverterSolarlogImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new PvInverterSolarlogImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // + .setId("meter0") // + .setModbusId("modbus0") // .build()) // ; } diff --git a/io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/AbstractSunSpecPvInverter.java b/io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/AbstractSunSpecPvInverter.java index 34ff14dc35d..836904c4da0 100644 --- a/io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/AbstractSunSpecPvInverter.java +++ b/io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/AbstractSunSpecPvInverter.java @@ -26,6 +26,7 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.sunspec.AbstractOpenemsSunSpecComponent; import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel; import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S101; @@ -45,7 +46,6 @@ import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.SinglePhase; import io.openems.edge.meter.api.SinglePhaseMeter; import io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter; diff --git a/io.openems.edge.scheduler.allalphabetically/test/io/openems/edge/scheduler/allalphabetically/SchedulerAllAlphabeticallyImplTest.java b/io.openems.edge.scheduler.allalphabetically/test/io/openems/edge/scheduler/allalphabetically/SchedulerAllAlphabeticallyImplTest.java index 204e4e48109..0b4ca7c84d5 100644 --- a/io.openems.edge.scheduler.allalphabetically/test/io/openems/edge/scheduler/allalphabetically/SchedulerAllAlphabeticallyImplTest.java +++ b/io.openems.edge.scheduler.allalphabetically/test/io/openems/edge/scheduler/allalphabetically/SchedulerAllAlphabeticallyImplTest.java @@ -18,33 +18,25 @@ public class SchedulerAllAlphabeticallyImplTest { - private static final String SCHEDULER_ID = "scheduler0"; - - private static final String CTRL0_ID = "ctrl0"; - private static final String CTRL1_ID = "ctrl1"; - private static final String CTRL2_ID = "ctrl2"; - private static final String CTRL3_ID = "ctrl3"; - private static final String CTRL4_ID = "ctrl4"; - @Test public void testWithFixedPriorities() throws Exception { final SchedulerAllAlphabetically sut = new SchedulerAllAlphabeticallyImpl(); new ComponentTest(sut) // .addReference("componentManager", new DummyComponentManager()) // - .addComponent(new DummyController(CTRL0_ID)) // - .addComponent(new DummyController(CTRL1_ID)) // - .addComponent(new DummyController(CTRL2_ID)) // - .addComponent(new DummyController(CTRL3_ID)) // - .addComponent(new DummyController(CTRL4_ID)) // + .addComponent(new DummyController("ctrl0")) // + .addComponent(new DummyController("ctrl1")) // + .addComponent(new DummyController("ctrl2")) // + .addComponent(new DummyController("ctrl3")) // + .addComponent(new DummyController("ctrl4")) // .activate(MyConfig.create() // - .setId(SCHEDULER_ID) // - .setControllersIds(CTRL2_ID, CTRL1_ID, "") // + .setId("scheduler0") // + .setControllersIds("ctrl2", "ctrl1", "") // .build()) .next(new TestCase()) // .deactivate(); assertEquals(// - Arrays.asList(CTRL2_ID, CTRL1_ID, CTRL0_ID, CTRL3_ID, CTRL4_ID), // + Arrays.asList("ctrl2", "ctrl1", "ctrl0", "ctrl3", "ctrl4"), // getControllerIds(sut)); } @@ -77,7 +69,7 @@ public void testOnlyAlphabeticalOrdering() throws Exception { test // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(SCHEDULER_ID) // + .setId("scheduler0") // .setControllersIds() // .build()) // .next(new TestCase()); diff --git a/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/test/AbstractDummyScheduler.java b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/test/AbstractDummyScheduler.java new file mode 100644 index 00000000000..530267953b4 --- /dev/null +++ b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/test/AbstractDummyScheduler.java @@ -0,0 +1,15 @@ +package io.openems.edge.scheduler.api.test; + +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.test.AbstractDummyOpenemsComponent; +import io.openems.edge.scheduler.api.Scheduler; + +public abstract class AbstractDummyScheduler> + extends AbstractDummyOpenemsComponent implements Scheduler, OpenemsComponent { + + protected AbstractDummyScheduler(String id, io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, + io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) { + super(id, firstInitialChannelIds, furtherInitialChannelIds); + } + +} diff --git a/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/test/DummyScheduler.java b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/test/DummyScheduler.java new file mode 100644 index 00000000000..00de718a1ad --- /dev/null +++ b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/test/DummyScheduler.java @@ -0,0 +1,46 @@ +package io.openems.edge.scheduler.api.test; + +import java.util.Arrays; +import java.util.LinkedHashSet; + +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.scheduler.api.Scheduler; + +/** + * Provides a simple, simulated {@link Scheduler} component that can be used + * together with the OpenEMS Component test framework. + */ +public class DummyScheduler extends AbstractDummyScheduler implements Scheduler, OpenemsComponent { + + private LinkedHashSet controllers = new LinkedHashSet<>(); + + public DummyScheduler(String id) { + super(id, // + OpenemsComponent.ChannelId.values() // + ); + } + + @Override + protected final DummyScheduler self() { + return this; + } + + /** + * Sets the {@link Controller}s. + * + * @param controllerIds Component-IDs of the Controllers + * @return myself + */ + public final DummyScheduler setControllers(String... controllerIds) { + var cs = new LinkedHashSet(); + Arrays.stream(controllerIds).forEach(c -> cs.add(c)); + this.controllers = cs; + return this.self(); + } + + @Override + public LinkedHashSet getControllers() { + return this.controllers; + } +} diff --git a/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/test/package-info.java b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/test/package-info.java new file mode 100644 index 00000000000..3a0c0e70c08 --- /dev/null +++ b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/test/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.scheduler.api.test; diff --git a/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/SchedulerDailyImplTest.java b/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/SchedulerDailyImplTest.java index 9f0d5e94cb1..f992c074e95 100644 --- a/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/SchedulerDailyImplTest.java +++ b/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/SchedulerDailyImplTest.java @@ -1,18 +1,16 @@ package io.openems.edge.scheduler.daily; +import static io.openems.common.utils.JsonUtils.buildJsonArray; +import static io.openems.common.utils.JsonUtils.buildJsonObject; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static java.time.temporal.ChronoUnit.HOURS; import static org.junit.Assert.assertEquals; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; import java.util.List; import org.junit.Test; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.test.TimeLeapClock; -import io.openems.common.utils.JsonUtils; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; @@ -21,56 +19,49 @@ public class SchedulerDailyImplTest { - private static final String SCHEDULER_ID = "scheduler0"; - - private static final String CTRL0_ID = "ctrl0"; - private static final String CTRL1_ID = "ctrl1"; - private static final String CTRL2_ID = "ctrl2"; - private static final String CTRL3_ID = "ctrl3"; - private static final String CTRL4_ID = "ctrl4"; - @Test public void test() throws Exception { - final var clock = new TimeLeapClock(Instant.parse("2020-01-01T00:00:00.00Z"), ZoneOffset.UTC); + final var clock = createDummyClock(); final SchedulerDaily sut = new SchedulerDailyImpl(); new ComponentTest(sut) // .addReference("componentManager", new DummyComponentManager(clock)) // - .addComponent(new DummyController(CTRL0_ID)) // - .addComponent(new DummyController(CTRL1_ID)) // - .addComponent(new DummyController(CTRL2_ID)) // - .addComponent(new DummyController(CTRL3_ID)) // - .addComponent(new DummyController(CTRL4_ID)) // + .addComponent(new DummyController("ctrl0")) // + .addComponent(new DummyController("ctrl1")) // + .addComponent(new DummyController("ctrl2")) // + .addComponent(new DummyController("ctrl3")) // + .addComponent(new DummyController("ctrl4")) // .activate(MyConfig.create() // - .setId(SCHEDULER_ID) // - .setAlwaysRunBeforeControllerIds(CTRL2_ID).setControllerScheduleJson(JsonUtils.buildJsonArray() // - .add(JsonUtils.buildJsonObject() // + .setId("scheduler0") // + .setAlwaysRunBeforeControllerIds("ctrl2") // + .setControllerScheduleJson(buildJsonArray() // + .add(buildJsonObject() // .addProperty("time", "08:00:00") // - .add("controllers", JsonUtils.buildJsonArray() // - .add(CTRL0_ID) // + .add("controllers", buildJsonArray() // + .add("ctrl0") // .build()) // .build()) // - .add(JsonUtils.buildJsonObject() // + .add(buildJsonObject() // .addProperty("time", "13:45:00") // - .add("controllers", JsonUtils.buildJsonArray() // - .add(CTRL4_ID) // + .add("controllers", buildJsonArray() // + .add("ctrl4") // .build()) // .build()) // .build().toString()) - .setAlwaysRunAfterControllerIds(CTRL3_ID, CTRL1_ID) // + .setAlwaysRunAfterControllerIds("ctrl3", "ctrl1") // .build()) // .next(new TestCase("00:00") // .onBeforeControllersCallbacks(() -> assertEquals(// - Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), // + List.of("ctrl2", "ctrl4", "ctrl3", "ctrl1"), // getControllerIds(sut)))) // .next(new TestCase("12:00") // - .timeleap(clock, 12, ChronoUnit.HOURS) // + .timeleap(clock, 12, HOURS) // .onBeforeControllersCallbacks(() -> assertEquals(// - Arrays.asList(CTRL2_ID, CTRL0_ID, CTRL3_ID, CTRL1_ID), // + List.of("ctrl2", "ctrl0", "ctrl3", "ctrl1"), // getControllerIds(sut)))) .next(new TestCase("14:00") // - .timeleap(clock, 12, ChronoUnit.HOURS) // + .timeleap(clock, 12, HOURS) // .onBeforeControllersCallbacks(() -> assertEquals(// - Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), // + List.of("ctrl2", "ctrl4", "ctrl3", "ctrl1"), // getControllerIds(sut)))); } diff --git a/io.openems.edge.scheduler.fixedorder/test/io/openems/edge/scheduler/fixedorder/SchedulerFixedOrderImplTest.java b/io.openems.edge.scheduler.fixedorder/test/io/openems/edge/scheduler/fixedorder/SchedulerFixedOrderImplTest.java index 47bc0a88a65..710c1adddc0 100644 --- a/io.openems.edge.scheduler.fixedorder/test/io/openems/edge/scheduler/fixedorder/SchedulerFixedOrderImplTest.java +++ b/io.openems.edge.scheduler.fixedorder/test/io/openems/edge/scheduler/fixedorder/SchedulerFixedOrderImplTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; -import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -40,7 +39,7 @@ public void test() throws Exception { test.next(new TestCase()); // assertEquals(// - Arrays.asList(CTRL3_ID, CTRL1_ID), // + List.of(CTRL3_ID, CTRL1_ID), // getControllerIds(sut)); } diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/DummyDatasource.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/DummyDatasource.java new file mode 100644 index 00000000000..0f1a58189f5 --- /dev/null +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/DummyDatasource.java @@ -0,0 +1,38 @@ +package io.openems.edge.simulator.datasource.api; + +import java.util.List; +import java.util.Set; + +import io.openems.common.types.ChannelAddress; +import io.openems.common.types.OpenemsType; + +public class DummyDatasource implements SimulatorDatasource { + + public final Object value; + + public DummyDatasource(Object value) { + this.value = value; + } + + @Override + public Set getKeys() { + return Set.of(); + } + + @Override + public int getTimeDelta() { + return 0; + } + + @SuppressWarnings("unchecked") + @Override + public List getValues(OpenemsType type, ChannelAddress channelAddress) { + return (List) List.of(this.value); + } + + @SuppressWarnings("unchecked") + @Override + public T getValue(OpenemsType type, ChannelAddress channelAddress) { + return (T) this.value; + } +} diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatorEvcs.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatorEvcs.java index 59c4acf3eef..bd93a48548f 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatorEvcs.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatorEvcs.java @@ -5,13 +5,12 @@ import io.openems.common.channel.Unit; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; -import io.openems.edge.common.channel.LongReadChannel; -import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.ManagedEvcs; +import io.openems.edge.meter.api.ElectricityMeter; -public interface SimulatorEvcs extends ManagedEvcs, Evcs, OpenemsComponent, EventHandler { +public interface SimulatorEvcs extends ManagedEvcs, Evcs, ElectricityMeter, OpenemsComponent, EventHandler { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { SIMULATED_CHARGE_POWER(Doc.of(OpenemsType.INTEGER).unit(Unit.WATT)); @@ -27,24 +26,4 @@ public Doc doc() { return this.doc; } } - - @Override - public default Value getActiveConsumptionEnergy() { - return this.getActiveConsumptionEnergyChannel().getNextValue(); - } - - @Override - public default void _setActiveConsumptionEnergy(long value) { - this.channel(Evcs.ChannelId.ACTIVE_CONSUMPTION_ENERGY).setNextValue(value); - } - - @Override - public default void _setActiveConsumptionEnergy(Long value) { - this.channel(Evcs.ChannelId.ACTIVE_CONSUMPTION_ENERGY).setNextValue(value); - } - - @Override - public default LongReadChannel getActiveConsumptionEnergyChannel() { - return this.channel(Evcs.ChannelId.ACTIVE_CONSUMPTION_ENERGY); - } } diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatorEvcsImpl.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatorEvcsImpl.java index 371c2ed8619..f554ad4dba9 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatorEvcsImpl.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatorEvcsImpl.java @@ -17,6 +17,7 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.evcs.api.AbstractManagedEvcsComponent; @@ -24,6 +25,7 @@ import io.openems.edge.evcs.api.EvcsPower; import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.Status; +import io.openems.edge.meter.api.ElectricityMeter; @Designate(ocd = Config.class, factory = true) @Component(// @@ -36,7 +38,7 @@ EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, // }) public class SimulatorEvcsImpl extends AbstractManagedEvcsComponent - implements SimulatorEvcs, ManagedEvcs, Evcs, OpenemsComponent, EventHandler { + implements SimulatorEvcs, ManagedEvcs, Evcs, ElectricityMeter, OpenemsComponent, EventHandler { @Reference private EvcsPower evcsPower; @@ -49,6 +51,7 @@ public class SimulatorEvcsImpl extends AbstractManagedEvcsComponent public SimulatorEvcsImpl() { super(// OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // ManagedEvcs.ChannelId.values(), // Evcs.ChannelId.values(), // SimulatorEvcs.ChannelId.values() // @@ -91,18 +94,18 @@ public void handleEvent(Event event) { private void updateChannels() { int chargePowerLimit = this.getSetChargePowerLimit().orElse(0); - this._setChargePower(chargePowerLimit); + this._setActivePower(chargePowerLimit); /* * Set Simulated "meter" Active Power */ - this._setChargePower(chargePowerLimit); + this._setActivePower(chargePowerLimit); /* * Set calculated energy */ var timeDiff = ChronoUnit.MILLIS.between(this.lastUpdate, LocalDateTime.now()); - var energyTransfered = timeDiff / 1000.0 / 60.0 / 60.0 * this.getChargePower().orElse(0); + var energyTransfered = timeDiff / 1000.0 / 60.0 / 60.0 * this.getActivePower().orElse(0); this.exactEnergySession = this.exactEnergySession + energyTransfered; this._setEnergySession((int) this.exactEnergySession); @@ -110,9 +113,14 @@ private void updateChannels() { this.lastUpdate = LocalDateTime.now(); } + @Override + public MeterType getMeterType() { + return MeterType.MANAGED_CONSUMPTION_METERED; + } + @Override public String debugLog() { - return this.getChargePower().asString(); + return this.getActivePower().asString(); } @Override @@ -138,7 +146,7 @@ public boolean getConfiguredDebugMode() { @Override public boolean applyChargePowerLimit(int power) throws OpenemsException { this._setSetChargePowerLimit(power); - this._setChargePower(power); + this._setActivePower(power); this._setStatus(power > 0 ? Status.CHARGING : Status.CHARGING_REJECTED); return true; } diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/Config.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/Config.java index 0c62512f586..abaa040ae8f 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/Config.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/Config.java @@ -20,6 +20,13 @@ @AttributeDefinition(name = "Datasource-ID", description = "ID of Simulator Datasource.") String datasource_id() default "datasource0"; + @AttributeDefinition(name = "Need frequency Step response?", description = "Need frequency Step response?") + boolean needFrequencyStepResponse() default false; + + @AttributeDefinition(name = "Start Time for frequency step response", description = "Time to Start(format: 2024-01-29 20:12:00). " + + "if the time specified is not in future or start time is not entered, Current time is used instead .") + String startTime() default "2024-01-29 20:12:00"; + @AttributeDefinition(name = "Datasource target filter", description = "This is auto-generated by 'Datasource-ID'.") String datasource_target() default "(enabled=true)"; diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActing.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActing.java index 578f38db722..c7b5b733618 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActing.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActing.java @@ -2,9 +2,13 @@ import org.osgi.service.event.EventHandler; +import io.openems.common.channel.PersistencePriority; import io.openems.common.channel.Unit; import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.StateChannel; +import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.timedata.api.TimedataProvider; @@ -12,6 +16,10 @@ public interface SimulatorGridMeterActing extends ElectricityMeter, OpenemsComponent, TimedataProvider, EventHandler { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + STATE_MACHINE(Doc.of(State.values()) // + .persistencePriority(PersistencePriority.HIGH)// + .text("Current State of State-Machine")), + SIMULATED_ACTIVE_POWER(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT)); @@ -26,4 +34,32 @@ public Doc doc() { return this.doc; } } + + /** + * Gets the Channel for {@link ChannelId#STATE_MACHINE}. + * + * @return the Channel + */ + public default Channel getStateMachineChannel() { + return this.channel(ChannelId.STATE_MACHINE); + } + + /** + * Gets the {@link StateChannel} for {@link ChannelId#STATE_MACHINE}. + * + * @return the Channel {@link Value} + */ + public default Value getStateMachine() { + return this.getStateMachineChannel().value(); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#STATE_MACHINE} + * Channel. + * + * @param value the next value + */ + public default void _setStateMachine(State value) { + this.getStateMachineChannel().setNextValue(value); + } } diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActingImpl.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActingImpl.java index 1a60a481d58..57df9b2454e 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActingImpl.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActingImpl.java @@ -1,6 +1,11 @@ package io.openems.edge.simulator.meter.grid.acting; import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -18,17 +23,20 @@ import org.osgi.service.event.EventHandler; import org.osgi.service.event.propertytypes.EventTopics; import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.openems.common.types.ChannelAddress; +import io.openems.common.types.MeterType; import io.openems.common.types.OpenemsType; 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.type.TypeUtils; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.ess.api.MetaEss; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.simulator.datasource.api.SimulatorDatasource; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; @@ -49,11 +57,18 @@ public class SimulatorGridMeterActingImpl extends AbstractOpenemsComponent implements SimulatorGridMeterActing, ElectricityMeter, OpenemsComponent, TimedataProvider, EventHandler { + private final Logger log = LoggerFactory.getLogger(SimulatorGridMeterActingImpl.class); private final CalculateEnergyFromPower calculateProductionEnergy = new CalculateEnergyFromPower(this, ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY); private final CalculateEnergyFromPower calculateConsumptionEnergy = new CalculateEnergyFromPower(this, ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY); + private StepResponseHandler stepResponseHandler; + private Config config = null; + + @Reference + private ComponentManager componentManager; + @Reference private ConfigurationAdmin cm; @@ -72,16 +87,23 @@ public SimulatorGridMeterActingImpl() { ElectricityMeter.ChannelId.values(), // SimulatorGridMeterActing.ChannelId.values() // ); + } @Activate private void activate(ComponentContext context, Config config) throws IOException { + this.config = config; super.activate(context, config.id(), config.alias(), config.enabled()); // update filter for 'datasource' if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "datasource", config.datasource_id())) { return; } + + if (this.config.needFrequencyStepResponse()) { + Instant startTime = this.convertTime(this.config.startTime()); + this.stepResponseHandler = new StepResponseHandler(this, startTime); + } } @Override @@ -103,6 +125,9 @@ public void handleEvent(Event event) { switch (event.getTopic()) { case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: this.updateChannels(); + if (this.config.needFrequencyStepResponse()) { + this.stepResponseHandler.doStepResponse(); + } break; case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE: this.calculateEnergy(); @@ -168,4 +193,44 @@ public Timedata getTimedata() { return this.timedata; } + /** + * Converts a string representation of time to an Instant object. + *

    + * If the input time is null or empty, the current time is returned. If the + * input time is successfully parsed, it is converted to an Instant object. If + * the parsed time is in the past, and the current time is returned. If there's + * an error parsing the input time, and the current time is returned. + *

    + * + * @param inputTime the string representation of time to be converted + * @return an Instant converted time + */ + public Instant convertTime(String inputTime) { + + Instant currentTime = this.getCurrentTime(); + if (inputTime == null || inputTime.isEmpty()) { + return currentTime; + } + try { + LocalDateTime localDateTime = LocalDateTime.parse(inputTime, + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + Instant futureTime = localDateTime.atZone(ZoneId.systemDefault()).toInstant(); + return currentTime.isAfter(futureTime) ? currentTime : futureTime; + } catch (DateTimeParseException e) { + this.log.error( + "Error parsing input time: " + inputTime + " instead current time: " + currentTime + " is taken."); + return currentTime; + } + } + + /** + * Retrieves the current time component manager. + * + * @return An Instant representing the current time. + */ + public Instant getCurrentTime() { + var currentTime = this.componentManager.getClock().withZone(ZoneId.systemDefault()); + return Instant.now(currentTime); + } + } diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/State.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/State.java new file mode 100644 index 00000000000..e5c26f2332c --- /dev/null +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/State.java @@ -0,0 +1,33 @@ +package io.openems.edge.simulator.meter.grid.acting; + +import io.openems.common.types.OptionsEnum; + +public enum State implements OptionsEnum { + UNDEFINED(-1), // + INITIAL_FREQ(1), // + FIRST_STEPDOWN_FREQUENCY(2), // + SECOND_STEPDOWN_FREQUENCY(3), // + FINISH(4); + + private final int value; + + private State(int value) { + this.value = value; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public OptionsEnum getUndefined() { + return UNDEFINED; + } + + @Override + public String getName() { + return this.name(); + } + +} diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/StepResponseHandler.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/StepResponseHandler.java new file mode 100644 index 00000000000..5a75825775e --- /dev/null +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/acting/StepResponseHandler.java @@ -0,0 +1,57 @@ +package io.openems.edge.simulator.meter.grid.acting; + +import java.time.Duration; +import java.time.Instant; + +public class StepResponseHandler { + + private State state; + private int repetitionCounter; + private SimulatorGridMeterActingImpl meter; + private Instant startTime; + + private static int TIME_THRESHOLD_SECONDS = 120; // [120 seconds in each step] + + public StepResponseHandler(SimulatorGridMeterActingImpl meter, Instant startTime) { + this.state = State.UNDEFINED; + // repeats atleast once + this.repetitionCounter = 1; + this.meter = meter; + this.startTime = startTime; + } + + /** + * State Machine for frequency step response. + */ + public void doStepResponse() { + switch (this.state) { + case UNDEFINED -> this.state = State.INITIAL_FREQ; + case INITIAL_FREQ -> this.handleStateTransition(50000, State.FIRST_STEPDOWN_FREQUENCY); + case FIRST_STEPDOWN_FREQUENCY -> this.handleStateTransition(49750, State.SECOND_STEPDOWN_FREQUENCY); + case SECOND_STEPDOWN_FREQUENCY -> this.handleStateTransition(49650, State.FINISH); + case FINISH -> { + if (this.repetitionCounter <= 1) { + this.handleFinishTransition(50000, State.FINISH); + } else { + this.repetitionCounter--; + this.handleFinishTransition(50000, State.INITIAL_FREQ); + } + } + } + this.meter._setStateMachine(this.state); + } + + private void handleStateTransition(int frequency, State nextState) { + var now = this.meter.getCurrentTime(); + this.meter._setFrequency(frequency); + if (Duration.between(this.startTime, now).getSeconds() > TIME_THRESHOLD_SECONDS) { + this.startTime = now; + this.state = nextState; + } + } + + private void handleFinishTransition(int frequency, State nextState) { + this.meter._setFrequency(frequency); + this.state = nextState; + } +} diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/reacting/SimulatorGridMeterReactingImpl.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/reacting/SimulatorGridMeterReactingImpl.java index 664e6d74428..0401d3e0999 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/reacting/SimulatorGridMeterReactingImpl.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/grid/reacting/SimulatorGridMeterReactingImpl.java @@ -19,6 +19,7 @@ import org.osgi.service.event.propertytypes.EventTopics; import org.osgi.service.metatype.annotations.Designate; +import io.openems.common.types.MeterType; import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; @@ -27,7 +28,6 @@ import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.ess.api.MetaEss; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.simulator.Constants; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; @@ -139,6 +139,7 @@ public void handleEvent(Event event) { try { switch (m.getMeterType()) { case CONSUMPTION_METERED: + case MANAGED_CONSUMPTION_METERED: case GRID: // ignore break; diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/nrc/acting/SimulatorNrcMeterActingImpl.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/nrc/acting/SimulatorNrcMeterActingImpl.java index c65c5bb54b0..3c33cf96b4f 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/nrc/acting/SimulatorNrcMeterActingImpl.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/nrc/acting/SimulatorNrcMeterActingImpl.java @@ -18,13 +18,13 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.types.ChannelAddress; +import io.openems.common.types.MeterType; import io.openems.common.types.OpenemsType; import io.openems.edge.common.component.AbstractOpenemsComponent; 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.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.simulator.datasource.api.SimulatorDatasource; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/production/acting/SimulatorProductionMeterActingImpl.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/production/acting/SimulatorProductionMeterActingImpl.java index 6ed67e2a82e..12f7a7dc1d9 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/production/acting/SimulatorProductionMeterActingImpl.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/meter/production/acting/SimulatorProductionMeterActingImpl.java @@ -18,13 +18,13 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.types.ChannelAddress; +import io.openems.common.types.MeterType; import io.openems.common.types.OpenemsType; import io.openems.edge.common.component.AbstractOpenemsComponent; 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.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.simulator.datasource.api.SimulatorDatasource; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/pvinverter/SimulatorPvInverterImpl.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/pvinverter/SimulatorPvInverterImpl.java index 822c2854765..a5678c811a6 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/pvinverter/SimulatorPvInverterImpl.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/pvinverter/SimulatorPvInverterImpl.java @@ -18,12 +18,12 @@ import org.osgi.service.metatype.annotations.Designate; import io.openems.common.types.ChannelAddress; +import io.openems.common.types.MeterType; import io.openems.common.types.OpenemsType; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter; import io.openems.edge.simulator.datasource.api.SimulatorDatasource; import io.openems.edge.timedata.api.Timedata; diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/battery/SimulatorBatteryImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/battery/SimulatorBatteryImplTest.java index 0762e332f6d..d2f19787009 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/battery/SimulatorBatteryImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/battery/SimulatorBatteryImplTest.java @@ -7,13 +7,11 @@ public class SimulatorBatteryImplTest { - private static final String COMPONENT_ID = "battery0"; - @Test public void test() throws Exception { new ComponentTest(new SimulatorBatteryImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("battery0") // .setCapacityKWh(20) // .setChargeMaxCurrent(40) // .setChargeMaxVoltage(700) // diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/csv/predefined/SimulatorDatasourceCsvPredefinedImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/csv/predefined/SimulatorDatasourceCsvPredefinedImplTest.java index 1ba5d54f0ca..c10d2e08e70 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/csv/predefined/SimulatorDatasourceCsvPredefinedImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/csv/predefined/SimulatorDatasourceCsvPredefinedImplTest.java @@ -9,14 +9,12 @@ public class SimulatorDatasourceCsvPredefinedImplTest { - private static final String COMPONENT_ID = "datasource0"; - @Test public void test() throws Exception { new ComponentTest(new SimulatorDatasourceCsvPredefinedImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("datasource0") // .setFactor(1) // .setFormat(CsvFormat.ENGLISH) // .setSource(Source.H0_HOUSEHOLD_SUMMER_WEEKDAY_NON_REGULATED_CONSUMPTION) // diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/single/direct/SimulatorDatasourceSingleDirectImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/single/direct/SimulatorDatasourceSingleDirectImplTest.java index 7ac397de8c1..2328e523a45 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/single/direct/SimulatorDatasourceSingleDirectImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/single/direct/SimulatorDatasourceSingleDirectImplTest.java @@ -8,14 +8,12 @@ public class SimulatorDatasourceSingleDirectImplTest { - private static final String COMPONENT_ID = "datasource0"; - @Test public void test() throws Exception { new ComponentTest(new SimulatorDatasourceSingleDirectImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("datasource0") // .setTimeDelta(0) // .setValues() // .build()) // diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/asymmetric/reacting/SimulatorEssAsymmetricReactingImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/asymmetric/reacting/SimulatorEssAsymmetricReactingImplTest.java index 8a2656a7343..35c7a7f745c 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/asymmetric/reacting/SimulatorEssAsymmetricReactingImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/asymmetric/reacting/SimulatorEssAsymmetricReactingImplTest.java @@ -8,22 +8,19 @@ import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.ess.test.DummyPower; import io.openems.edge.ess.test.ManagedSymmetricEssTest; -import io.openems.edge.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImpl; +import io.openems.edge.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImplTest; public class SimulatorEssAsymmetricReactingImplTest { - private static final String ESS_ID = "ess0"; - private static final String DATASOURCE_ID = "datasource0"; - @Test public void test() throws OpenemsException, Exception { new ManagedSymmetricEssTest(new SimulatorEssAsymmetricReactingImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("datasource", new SimulatorDatasourceCsvDirectImpl()) // + .addReference("datasource", SimulatorDatasourceCsvDirectImplTest.create("datasource0", "123")) // .addReference("power", new DummyPower()) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setDatasourceId(DATASOURCE_ID) // + .setId("ess0") // + .setDatasourceId("datasource0") // .setCapacity(10_000) // .setMaxApparentPower(10_000) // .setInitialSoc(50) // diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/singlephase/reacting/SimulatorEssSinglePhaseReactingImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/singlephase/reacting/SimulatorEssSinglePhaseReactingImplTest.java index d00be5bce1c..38be1552e53 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/singlephase/reacting/SimulatorEssSinglePhaseReactingImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/singlephase/reacting/SimulatorEssSinglePhaseReactingImplTest.java @@ -9,7 +9,7 @@ import io.openems.edge.ess.api.SinglePhase; import io.openems.edge.ess.test.DummyPower; import io.openems.edge.ess.test.ManagedSymmetricEssTest; -import io.openems.edge.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImpl; +import io.openems.edge.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImplTest; public class SimulatorEssSinglePhaseReactingImplTest { @@ -20,7 +20,7 @@ public class SimulatorEssSinglePhaseReactingImplTest { public void test() throws OpenemsException, Exception { new ManagedSymmetricEssTest(new SimulatorEssSinglePhaseReactingImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("datasource", new SimulatorDatasourceCsvDirectImpl()) // + .addReference("datasource", SimulatorDatasourceCsvDirectImplTest.create("datasource0", "123")) // .addReference("power", new DummyPower()) // .activate(MyConfig.create() // .setId(ESS_ID) // diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/evcs/SimulatorEvcsImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/evcs/SimulatorEvcsImplTest.java index 5090dd90d7d..4aed1982e28 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/evcs/SimulatorEvcsImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/evcs/SimulatorEvcsImplTest.java @@ -10,15 +10,13 @@ public class SimulatorEvcsImplTest { - private static final String ESS_ID = "evcs0"; - @Test public void test() throws OpenemsException, Exception { new ComponentTest(new SimulatorEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("evcsPower", new DummyEvcsPower()) // .activate(MyConfig.create() // - .setId(ESS_ID) // + .setId("evcs0") // .setMinHwPower(1000) // .setMaxHwPower(10000) // .build()) // diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/io/SimulatorIoDigitalInputOutputImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/io/SimulatorIoDigitalInputOutputImplTest.java index c147bc6a0b1..bd83f9838c9 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/io/SimulatorIoDigitalInputOutputImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/io/SimulatorIoDigitalInputOutputImplTest.java @@ -8,13 +8,11 @@ public class SimulatorIoDigitalInputOutputImplTest { - private static final String COMPONENT_ID = "io0"; - @Test public void test() throws OpenemsException, Exception { new ComponentTest(new SimulatorIoDigitalInputOutputImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("io0") // .setNumberOfOutputs(3) // .build()) // .next(new TestCase()); // diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/acting/MyConfig.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/acting/MyConfig.java index 6367e673ebd..04a1c548104 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/acting/MyConfig.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/acting/MyConfig.java @@ -9,6 +9,8 @@ public class MyConfig extends AbstractComponentConfig implements Config { protected static class Builder { private String id; private String datasourceId; + private String startTime; + private boolean needFrequencyStepResponse; private Builder() { } @@ -23,6 +25,16 @@ public Builder setDatasourceId(String datasourceId) { return this; } + public Builder setStartTime(String startTime) { + this.startTime = startTime; + return this; + } + + public Builder needFrequencyStepResponse(boolean needFrequencyStepResponse) { + this.needFrequencyStepResponse = needFrequencyStepResponse; + return this; + } + public MyConfig build() { return new MyConfig(this); } @@ -54,4 +66,14 @@ public String datasource_target() { return ConfigUtils.generateReferenceTargetFilter(this.id(), this.datasource_id()); } + @Override + public boolean needFrequencyStepResponse() { + return this.builder.needFrequencyStepResponse; + } + + @Override + public String startTime() { + return this.builder.startTime; + } + } \ No newline at end of file diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActingImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActingImplTest.java index 32fa540b54c..dd2d9614318 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActingImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/acting/SimulatorGridMeterActingImplTest.java @@ -1,26 +1,48 @@ package io.openems.edge.simulator.meter.grid.acting; +import java.time.Instant; +import java.time.ZoneOffset; + import org.junit.Test; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.test.TimeLeapClock; +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.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImpl; +import io.openems.edge.simulator.datasource.api.DummyDatasource; public class SimulatorGridMeterActingImplTest { - private static final String COMPONENT_ID = "meter0"; - private static final String DATASOURCE_ID = "datasource0"; - @Test public void test() throws OpenemsException, Exception { new ComponentTest(new SimulatorGridMeterActingImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("datasource", new SimulatorDatasourceCsvDirectImpl()) // + .addReference("datasource", new DummyDatasource(123)) // + .activate(MyConfig.create() // + .setId("meter0") // + .setStartTime("")// + .needFrequencyStepResponse(false)// + .setDatasourceId("datasource0") // + .build()) // + .next(new TestCase()) // + .deactivate(); + } + + @Test + public void test1() throws OpenemsException, Exception { + final var clock = new TimeLeapClock(Instant.parse("2024-01-29T19:05:00Z"), ZoneOffset.UTC); + new ComponentTest(new SimulatorGridMeterActingImpl()) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("datasource", new DummyDatasource(123)) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setDatasourceId(DATASOURCE_ID) // - .build()); // - // .next(new TestCase()); // TODO requires DummyDatasource + .setId("meter0") // + .setStartTime("")// + .needFrequencyStepResponse(true)// + .setDatasourceId("datasource0") // + .build()) // + .deactivate(); } } diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/reacting/SimulatorGridMeterReactingImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/reacting/SimulatorGridMeterReactingImplTest.java index 96222001f3e..dedc66db72d 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/reacting/SimulatorGridMeterReactingImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/meter/grid/reacting/SimulatorGridMeterReactingImplTest.java @@ -9,14 +9,12 @@ public class SimulatorGridMeterReactingImplTest { - private static final String COMPONENT_ID = "meter0"; - @Test public void test() throws OpenemsException, Exception { new ComponentTest(new SimulatorGridMeterReactingImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("meter0") // .build()) // .next(new TestCase()); } diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/modbus/SimulatorModbusImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/modbus/SimulatorModbusImplTest.java index 3a60a20f877..6f059aa6a80 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/modbus/SimulatorModbusImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/modbus/SimulatorModbusImplTest.java @@ -8,13 +8,11 @@ public class SimulatorModbusImplTest { - private static final String COMPONENT_ID = "modbus0"; - @Test public void test() throws OpenemsException, Exception { new ComponentTest(new SimulatorModbusImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("modbus0") // .build()) // .next(new TestCase()); } diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/pvinverter/SimulatorPvInverterImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/pvinverter/SimulatorPvInverterImplTest.java index 5310e732d61..9294bd195a1 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/pvinverter/SimulatorPvInverterImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/pvinverter/SimulatorPvInverterImplTest.java @@ -9,17 +9,14 @@ public class SimulatorPvInverterImplTest { - private static final String COMPONENT_ID = "pvInverter0"; - private static final String DATASOURCE_ID = "datasource0"; - @Test public void test() throws OpenemsException, Exception { new ComponentTest(new SimulatorPvInverterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("datasource", new SimulatorDatasourceCsvDirectImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setDatasourceId(DATASOURCE_ID) // + .setId("pvInverter0") // + .setDatasourceId("datasource0") // .build()); // // .next(new TestCase()); // TODO requires DummyDatasource } diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/thermometer/SimulatorThermometerImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/thermometer/SimulatorThermometerImplTest.java index c07899add9f..a6620e2fd54 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/thermometer/SimulatorThermometerImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/thermometer/SimulatorThermometerImplTest.java @@ -8,13 +8,11 @@ public class SimulatorThermometerImplTest { - private static final String COMPONENT_ID = "thermometer0"; - @Test public void test() throws OpenemsException, Exception { new ComponentTest(new SimulatorThermometerImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("thermometer0") // .setTemperature(20) // .build()) // .next(new TestCase()); diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/timedata/SimulatorTimedataImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/timedata/SimulatorTimedataImplTest.java index bd1d9358847..13d83a24d12 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/timedata/SimulatorTimedataImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/timedata/SimulatorTimedataImplTest.java @@ -9,13 +9,11 @@ public class SimulatorTimedataImplTest { - private static final String COMPONENT_ID = "thermometer0"; - @Test public void test() throws OpenemsException, Exception { new ComponentTest(new SimulatorTimedataImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("thermometer0") // .setFilename("") // .setFormat(CsvFormat.ENGLISH) // .build()) // diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/gridmeter/Config.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/gridmeter/Config.java index e8c2aa5e28f..fbbc7def519 100644 --- a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/gridmeter/Config.java +++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/gridmeter/Config.java @@ -3,7 +3,7 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.meter.api.MeterType; +import io.openems.common.types.MeterType; @ObjectClassDefinition(name = "Meter SolarEdge", // description = "Implements the SolarEdge Meter.") diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/gridmeter/SolarEdgeGridMeterImpl.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/gridmeter/SolarEdgeGridMeterImpl.java index fdb83a0ae8f..555a15013b9 100644 --- a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/gridmeter/SolarEdgeGridMeterImpl.java +++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/gridmeter/SolarEdgeGridMeterImpl.java @@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableMap; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ModbusComponent; import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel; @@ -26,7 +27,6 @@ import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.sunspec.AbstractSunSpecMeter; @Designate(ocd = Config.class, factory = true) diff --git a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/gridmeter/MyConfig.java b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/gridmeter/MyConfig.java index 99558daca4c..15e59eb4c4c 100644 --- a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/gridmeter/MyConfig.java +++ b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/gridmeter/MyConfig.java @@ -1,8 +1,8 @@ package io.openems.edge.solaredge.gridmeter; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; import io.openems.common.utils.ConfigUtils; -import io.openems.edge.meter.api.MeterType; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/gridmeter/SolarEdgeGridMeterImplTest.java b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/gridmeter/SolarEdgeGridMeterImplTest.java index 6c4ccbc5b0d..c04d3b3b5a4 100644 --- a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/gridmeter/SolarEdgeGridMeterImplTest.java +++ b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/gridmeter/SolarEdgeGridMeterImplTest.java @@ -1,27 +1,25 @@ package io.openems.edge.solaredge.gridmeter; +import static io.openems.common.types.MeterType.GRID; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.meter.api.MeterType; public class SolarEdgeGridMeterImplTest { - private static final String METER_ID = "meter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new SolarEdgeGridMeterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(METER_ID) // - .setModbusId(MODBUS_ID) // + .setId("meter0") // + .setModbusId("modbus0") // .setModbusUnitId(1) // - .setType(MeterType.GRID) // + .setType(GRID) // .build()) // ; } diff --git a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/pvinverter/SolarEdgePvInverterImplTest.java b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/pvinverter/SolarEdgePvInverterImplTest.java index c90b0a9e978..438399b1243 100644 --- a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/pvinverter/SolarEdgePvInverterImplTest.java +++ b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/pvinverter/SolarEdgePvInverterImplTest.java @@ -1,28 +1,26 @@ package io.openems.edge.solaredge.pvinverter; +import static io.openems.edge.pvinverter.sunspec.Phase.L1; + import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.pvinverter.sunspec.Phase; public class SolarEdgePvInverterImplTest { - private static final String PV_INVERTER_ID = "pvInverter0"; - private static final String MODBUS_ID = "modbus0"; - @Test public void test() throws Exception { new ComponentTest(new SolarEdgePvInverterImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("setModbus", new DummyModbusBridge("modbus0")) // .activate(MyConfig.create() // - .setId(PV_INVERTER_ID) // + .setId("pvInverter0") // .setReadOnly(true) // - .setModbusId(MODBUS_ID) // + .setModbusId("modbus0") // .setModbusUnitId(1) // - .setPhase(Phase.L1) // + .setPhase(L1) // .build()) // ; } diff --git a/io.openems.edge.tesla.powerwall2/test/io/openems/edge/tesla/powerwall2/battery/TeslaPowerwall2BatteryImplTest.java b/io.openems.edge.tesla.powerwall2/test/io/openems/edge/tesla/powerwall2/battery/TeslaPowerwall2BatteryImplTest.java index 1a3d4b39b01..d8deb4b3dcb 100644 --- a/io.openems.edge.tesla.powerwall2/test/io/openems/edge/tesla/powerwall2/battery/TeslaPowerwall2BatteryImplTest.java +++ b/io.openems.edge.tesla.powerwall2/test/io/openems/edge/tesla/powerwall2/battery/TeslaPowerwall2BatteryImplTest.java @@ -10,21 +10,18 @@ public class TeslaPowerwall2BatteryImplTest { - private static final String COMPONENT_ID = "ess0"; - private static final String CORE_ID = "core0"; - @Test public void test() throws Exception { new ComponentTest(new TeslaPowerwall2BatteryImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("core", new TeslaPowerwall2CoreImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setCoreId(CORE_ID) // + .setId("ess0") // + .setCoreId("core0") // .setPhase(SinglePhase.L1) // .build()) // .next(new TestCase()) // - ; + .deactivate(); } } diff --git a/io.openems.edge.tesla.powerwall2/test/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImplTest.java b/io.openems.edge.tesla.powerwall2/test/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImplTest.java index 99c4be260cd..df66e5fcfa6 100644 --- a/io.openems.edge.tesla.powerwall2/test/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImplTest.java +++ b/io.openems.edge.tesla.powerwall2/test/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImplTest.java @@ -7,17 +7,15 @@ public class TeslaPowerwall2CoreImplTest { - private static final String COMPONENT_ID = "core0"; - @Test public void test() throws Exception { new ComponentTest(new TeslaPowerwall2CoreImpl()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("core0") // .setIpAddress("127.0.0.1") // .setPort(443) // .build()) // .next(new TestCase()) // - ; + .deactivate(); } } diff --git a/io.openems.edge.timedata.influxdb/test/io/openems/edge/timedata/influxdb/TimedataInfluxDbImplTest.java b/io.openems.edge.timedata.influxdb/test/io/openems/edge/timedata/influxdb/TimedataInfluxDbImplTest.java index 4c097c21c69..05890b4b34c 100644 --- a/io.openems.edge.timedata.influxdb/test/io/openems/edge/timedata/influxdb/TimedataInfluxDbImplTest.java +++ b/io.openems.edge.timedata.influxdb/test/io/openems/edge/timedata/influxdb/TimedataInfluxDbImplTest.java @@ -1,19 +1,18 @@ package io.openems.edge.timedata.influxdb; +import static io.openems.common.channel.PersistencePriority.MEDIUM; +import static io.openems.shared.influxdb.QueryLanguageConfig.INFLUX_QL; + import org.junit.Test; -import io.openems.common.channel.PersistencePriority; import io.openems.common.oem.DummyOpenemsEdgeOem; 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.DummyCycle; -import io.openems.shared.influxdb.QueryLanguageConfig; public class TimedataInfluxDbImplTest { - private static final String COMPONENT_ID = "influx0"; - @Test public void test() throws Exception { new ComponentTest(new TimedataInfluxDbImpl()) // @@ -21,8 +20,8 @@ public void test() throws Exception { .addReference("cycle", new DummyCycle(1000)) // .addReference("oem", new DummyOpenemsEdgeOem()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setQueryLanguage(QueryLanguageConfig.INFLUX_QL) // + .setId("influx0") // + .setQueryLanguage(INFLUX_QL) // .setUrl("http://localhost:8086") // .setOrg("-") // .setApiKey("username:password") // @@ -31,7 +30,7 @@ public void test() throws Exception { .setNoOfCycles(1) // .setMaxQueueSize(5000) // .setReadOnly(false) // - .setPersistencePriority(PersistencePriority.MEDIUM) + .setPersistencePriority(MEDIUM) // .build()) // .next(new TestCase()) // ; diff --git a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jReadHandler.java b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jReadHandler.java index e12637892de..0be558ae28e 100644 --- a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jReadHandler.java +++ b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jReadHandler.java @@ -552,8 +552,7 @@ public CompletableFuture> getLatestValue(// try { channel = this.componentManager.getChannel(channelAddress); } catch (Exception e) { - // unable to get channel - this.log.warn("Unable to query RRD4j", e); + this.log.warn("Unable to query [" + channelAddress + "] from RRD4j: " + e.getMessage()); return Optional.empty(); } diff --git a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jSupplier.java b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jSupplier.java index fc0d4f2b50b..06c62d9e289 100644 --- a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jSupplier.java +++ b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jSupplier.java @@ -143,7 +143,7 @@ public static ChannelDef getDsDefForChannel(final Unit channelUnit) { MICROVOLT, MILLIAMPERE_HOURS, MILLIAMPERE, MILLIHERTZ, MILLIOHM, MILLISECONDS, MILLIVOLT, MILLIWATT, MINUTE, NONE, WATT, VOLT, VOLT_AMPERE, VOLT_AMPERE_REACTIVE, WATT_HOURS_BY_WATT_PEAK, OHM, SECONDS, THOUSANDTH, WATT_HOURS, KILOWATT_HOURS, VOLT_AMPERE_HOURS, VOLT_AMPERE_REACTIVE_HOURS, - KILOVOLT_AMPERE_REACTIVE_HOURS, BAR -> + KILOVOLT_AMPERE_REACTIVE_HOURS, BAR, MILLIBAR -> new ChannelDef(DsType.GAUGE, Double.NaN, Double.NaN, ConsolFun.AVERAGE); case PERCENT -> new ChannelDef(DsType.GAUGE, 0, 100, ConsolFun.AVERAGE); case ON_OFF -> new ChannelDef(DsType.GAUGE, 0, 1, ConsolFun.AVERAGE); diff --git a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/DummyRecordWorkerFactory.java b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/DummyRecordWorkerFactory.java index b4af1e2e5a3..90492eead0f 100644 --- a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/DummyRecordWorkerFactory.java +++ b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/DummyRecordWorkerFactory.java @@ -1,20 +1,18 @@ package io.openems.edge.timedata.rrd4j; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.common.component.ComponentManager; public class DummyRecordWorkerFactory extends RecordWorkerFactory { - public DummyRecordWorkerFactory(ComponentManager componentManager) - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyRecordWorkerFactory(ComponentManager componentManager) throws ReflectionException { super(); - ReflectionUtils.setAttribute(RecordWorkerFactory.class, this, "cso", - new DummyRecordWorkerCso(componentManager)); + setAttributeViaReflection(this, "cso", new DummyRecordWorkerCso(componentManager)); } private static class DummyRecordWorkerCso implements ComponentServiceObjects { @@ -29,11 +27,7 @@ public DummyRecordWorkerCso(ComponentManager componentManager) { @Override public RecordWorker getService() { final var worker = new RecordWorker(); - try { - ReflectionUtils.setAttribute(RecordWorker.class, worker, "componentManager", this.componentManager); - } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } + setAttributeViaReflection(worker, "componentManager", this.componentManager); return worker; } diff --git a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jReadHandlerTest.java b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jReadHandlerTest.java index f0c0eab0d27..c9135ed3ab4 100644 --- a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jReadHandlerTest.java +++ b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jReadHandlerTest.java @@ -1,5 +1,6 @@ package io.openems.edge.timedata.rrd4j; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import static java.util.stream.Collectors.toMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -27,7 +28,6 @@ import io.openems.common.timedata.Resolution; import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; -import io.openems.common.utils.ReflectionUtils; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.test.AbstractDummyOpenemsComponent; @@ -80,9 +80,9 @@ public void setUp() throws Exception { final var versionHandler = new VersionHandler(); versionHandler.bindVersion(version3); - ReflectionUtils.setAttribute(Rrd4jReadHandler.class, this.readHandler, "componentManager", dcm); - ReflectionUtils.setAttribute(Rrd4jReadHandler.class, this.readHandler, "rrd4jSupplier", rrd4jSupplier); - ReflectionUtils.setAttribute(Rrd4jSupplier.class, rrd4jSupplier, "versionHandler", versionHandler); + setAttributeViaReflection(this.readHandler, "componentManager", dcm); + setAttributeViaReflection(this.readHandler, "rrd4jSupplier", rrd4jSupplier); + setAttributeViaReflection(rrd4jSupplier, "versionHandler", versionHandler); } diff --git a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/TimedataRrd4jImplTest.java b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/TimedataRrd4jImplTest.java index 980994dfb48..2b0590abc1b 100644 --- a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/TimedataRrd4jImplTest.java +++ b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/TimedataRrd4jImplTest.java @@ -1,5 +1,6 @@ package io.openems.edge.timedata.rrd4j; +import static io.openems.common.channel.PersistencePriority.MEDIUM; import static org.junit.Assert.assertEquals; import java.io.IOException; @@ -15,15 +16,12 @@ import org.rrd4j.core.RrdDef; import org.rrd4j.core.RrdMemoryBackendFactory; -import io.openems.common.channel.PersistencePriority; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; public class TimedataRrd4jImplTest { - private static final String COMPONENT_ID = "rrd4j0"; - @Test public void test() throws Exception { final var componentManager = new DummyComponentManager(); @@ -31,8 +29,8 @@ public void test() throws Exception { .addReference("workerFactory", new DummyRecordWorkerFactory(componentManager)) // .addReference("readHandler", new Rrd4jReadHandler()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // - .setPersistencePriority(PersistencePriority.MEDIUM) // + .setId("rrd4j0") // + .setPersistencePriority(MEDIUM) // .build()) // .next(new TestCase()) // ; diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApi.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApi.java index bdb5eeb4575..6f3c4b3e727 100644 --- a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApi.java +++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApi.java @@ -1,12 +1,17 @@ package io.openems.edge.timeofusetariff.api.utils; -import static io.openems.common.utils.JsonUtils.getAsDouble; -import static io.openems.common.utils.JsonUtils.getAsJsonObject; -import static io.openems.common.utils.JsonUtils.parseToJsonObject; +import static io.openems.common.utils.XmlUtils.getXmlRootDocument; +import static io.openems.common.utils.XmlUtils.stream; import java.io.IOException; +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.utils.XmlUtils; import io.openems.edge.common.currency.Currency; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -17,31 +22,58 @@ *

    * Day ahead prices retrieved from ENTSO-E are usually in EUR and might have to * be converted to the user's currency using the exchange rates provided by - * Exchange Rate API. For more information on the ExchangeRate API, visit: - * https://exchangerate.host/#/docs + * European Central Bank. */ // TODO this should be extracted to a Exchange-Rate API + Provider public class ExchangeRateApi { - private static final String BASE_URL = "http://api.exchangerate.host/live?access_key=%s&source=%s¤cies=%s"; + private static final String ECB_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"; + + // ECB gives exchange rates based on the EUR. + private static final Currency BASE_CURRENCY = Currency.EUR; private static final OkHttpClient client = new OkHttpClient(); /** - * Fetches the exchange rate from exchangerate.host. + * Fetches the exchange rate from ECB API. + * + * @param source the source currency (e.g. EUR) + * @param target the target currency (e.g. SEK) + * @param orElse the default value + * @return the exchange rate. + */ + public static double getExchangeRateOrElse(String source, Currency target, double orElse) { + try { + return getExchangeRate(source, target); + } catch (Exception e) { + e.printStackTrace(); + return orElse; + } + } + + /** + * Fetches the exchange rate from ECB API. * - * @param accessKey personal API access key. - * @param source the source currency (e.g. EUR) - * @param target the target currency (e.g. SEK) + * @param source the source currency (e.g. EUR) + * @param target the target currency (e.g. SEK) * @return the exchange rate. - * @throws IOException on error. - * @throws OpenemsNamedException on error + * @throws IOException on error + * @throws OpenemsNamedException on error + * @throws SAXException on error + * @throws ParserConfigurationException on error */ - public static double getExchangeRate(String accessKey, String source, Currency target) - throws IOException, OpenemsNamedException { + public static double getExchangeRate(String source, Currency target) + throws IOException, OpenemsNamedException, ParserConfigurationException, SAXException { + if (target == Currency.UNDEFINED) { + throw new OpenemsException("Global Currency is UNDEFINED. Please configure it in Core.Meta component"); + } + + if (target.name().equals(source) || target.equals(BASE_CURRENCY)) { + return 1.; // No need to fetch exchange rate from API + } + var request = new Request.Builder() // - .url(String.format(BASE_URL, accessKey, source, target.name())) // + .url(ECB_URL) // .build(); try (var response = client.newCall(request).execute()) { @@ -49,25 +81,34 @@ public static double getExchangeRate(String accessKey, String source, Currency t throw new IOException("Failed to fetch exchange rate. HTTP status code: " + response.code()); } - return parseResponse(response.body().string(), source, target); + return parseResponse(response.body().string(), target); } } /** - * Parses the response string from exchangerate.host. + * Parses the response string from ECB API. * * @param response the response string - * @param source the source currency (e.g. EUR) * @param target the target currency (e.g. SEK) * @return the exchange rate. - * @throws OpenemsNamedException on error. + * @throws IOException on error. + * @throws SAXException on error. + * @throws ParserConfigurationException on error. */ - protected static double parseResponse(String response, String source, Currency target) - throws OpenemsNamedException { - var json = parseToJsonObject(response); - var quotes = getAsJsonObject(json, "quotes"); - var result = getAsDouble(quotes, source + target.name()); - return result; + protected static double parseResponse(String response, Currency target) + throws ParserConfigurationException, SAXException, IOException { + var root = getXmlRootDocument(response); + return stream(root) // + .filter(n -> n.getNodeName() == "Cube") // Filter all elements + .flatMap(XmlUtils::stream) // Stream the child nodes of each + .filter(n -> n.getNodeName().equals("Cube")) // + .flatMap(XmlUtils::stream) // + .filter(n -> n.getNodeName().equals("Cube")) // + .filter(n -> n.getAttributes().getNamedItem("currency").getNodeValue().equals(target.name())) // + .map(n -> n.getAttributes().getNamedItem("rate").getNodeValue()) // + .mapToDouble(Double::parseDouble) // + .findFirst() // + .getAsDouble(); } } diff --git a/io.openems.edge.timeofusetariff.api/test/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApiTest.java b/io.openems.edge.timeofusetariff.api/test/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApiTest.java index 81657b127c0..7d0ee419286 100644 --- a/io.openems.edge.timeofusetariff.api/test/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApiTest.java +++ b/io.openems.edge.timeofusetariff.api/test/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApiTest.java @@ -5,40 +5,72 @@ import java.io.IOException; +import javax.xml.parsers.ParserConfigurationException; + import org.junit.Ignore; import org.junit.Test; +import org.xml.sax.SAXException; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.oem.DummyOpenemsEdgeOem; import io.openems.edge.common.currency.Currency; public class ExchangeRateApiTest { private static final String RESPONSE = """ - { - "success": true, - "terms": "https://currencylayer.com/terms", - "privacy": "https://currencylayer.com/privacy", - "timestamp": 1699605243, - "source": "EUR", - "quotes": { - "EURSEK": 11.649564 - } - } - """; + + Reference rates + + European Central Bank + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; // Remove '@Ignore' tag to test this API call. @Ignore @Test - public void testGetExchangeRate() throws IOException, OpenemsNamedException { - var oem = new DummyOpenemsEdgeOem(); - var rate = getExchangeRate(oem.getExchangeRateAccesskey(), "EUR", Currency.SEK); + public void testGetExchangeRate() throws IOException, OpenemsNamedException, ParserConfigurationException, SAXException { + var rate = getExchangeRate("EUR", Currency.SEK); System.out.println(rate); } @Test - public void testParseResponse() throws OpenemsNamedException { - var rate = ExchangeRateApi.parseResponse(RESPONSE, "EUR", Currency.SEK); - assertEquals(11.649564, rate, 0.0001); + public void testParseResponse() + throws OpenemsNamedException, ParserConfigurationException, SAXException, IOException { + var rate = ExchangeRateApi.parseResponse(RESPONSE, Currency.SEK); + assertEquals(11.4475, rate, 0.0001); } } diff --git a/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/TimeOfUseTariffAwattarImplTest.java b/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/TimeOfUseTariffAwattarImplTest.java index eaef278769f..dd329cfe509 100644 --- a/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/TimeOfUseTariffAwattarImplTest.java +++ b/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/TimeOfUseTariffAwattarImplTest.java @@ -1,5 +1,6 @@ package io.openems.edge.timeofusetariff.awattar; +import static io.openems.edge.timeofusetariff.awattar.Zone.GERMANY; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; @@ -11,15 +12,13 @@ public class TimeOfUseTariffAwattarImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { var awattar = new TimeOfUseTariffAwattarImpl(); new ComponentTest(awattar) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setZone(Zone.GERMANY) // + .setId("ctrl0") // + .setZone(GERMANY) // .build()) // ; } diff --git a/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/TimeOfUseTariffCorrentlyImplTest.java b/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/TimeOfUseTariffCorrentlyImplTest.java index 3516d5f8378..83c69c1e2eb 100644 --- a/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/TimeOfUseTariffCorrentlyImplTest.java +++ b/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/TimeOfUseTariffCorrentlyImplTest.java @@ -12,14 +12,12 @@ public class TimeOfUseTariffCorrentlyImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { var corrently = new TimeOfUseTariffCorrentlyImpl(); new ComponentTest(corrently) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setZipcode("94469" /* Deggendorf, Germany */) // .build()) // ; diff --git a/io.openems.edge.timeofusetariff.entsoe/readme.adoc b/io.openems.edge.timeofusetariff.entsoe/readme.adoc index aea3aee6223..83b1738c11c 100644 --- a/io.openems.edge.timeofusetariff.entsoe/readme.adoc +++ b/io.openems.edge.timeofusetariff.entsoe/readme.adoc @@ -6,6 +6,6 @@ To request a (free) authentication token, please see chapter "2. Authentication Prices retrieved from ENTSO-E are subsequently converted to the user's currency (defined in Core.Meta Component) using the Exchange Rates API. -For detailed information about the Exchange Rates API, please refer to: https://exchangerate.host/#/docs +For detailed information about the Exchange Rates API, please refer to: https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.timeofusetariff.entsoe[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java index 73a451dee78..e0f5ac2ac9b 100644 --- a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java +++ b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java @@ -21,9 +21,6 @@ @AttributeDefinition(name = "Security Token", description = "Security token for the ENTSO-E Transparency Platform", type = AttributeType.PASSWORD) String securityToken() default ""; - @AttributeDefinition(name = "Exchangerate.host API Access Key", description = "Access key for Exchangerate.host: Please register at https://exchangerate.host/ to get your personal access key", type = AttributeType.PASSWORD) - String exchangerateAccesskey() default ""; - @AttributeDefinition(name = "Bidding Zone", description = "Zone corresponding to the customer's location") BiddingZone biddingZone(); diff --git a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java index 873b6357fa7..fda5777bdd5 100644 --- a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java +++ b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java @@ -1,7 +1,7 @@ package io.openems.edge.timeofusetariff.entsoe; import static io.openems.common.utils.StringUtils.definedOrElse; -import static io.openems.edge.timeofusetariff.api.utils.ExchangeRateApi.getExchangeRate; +import static io.openems.edge.timeofusetariff.api.utils.ExchangeRateApi.getExchangeRateOrElse; import static io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils.generateDebugLog; import static io.openems.edge.timeofusetariff.entsoe.Utils.parseCurrency; import static io.openems.edge.timeofusetariff.entsoe.Utils.parsePrices; @@ -30,14 +30,11 @@ import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; import io.openems.common.oem.OpenemsEdgeOem; import io.openems.common.utils.ThreadPoolUtils; import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; -import io.openems.edge.common.currency.Currency; import io.openems.edge.common.meta.Meta; import io.openems.edge.timeofusetariff.api.TimeOfUsePrices; import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; @@ -64,7 +61,6 @@ public class TouEntsoeImpl extends AbstractOpenemsComponent implements TouEntsoe private Config config = null; private String securityToken = null; - private String exchangerateAccesskey = null; private ScheduledFuture future = null; public TouEntsoeImpl() { @@ -92,11 +88,6 @@ private void activate(ComponentContext context, Config config) { return; } - this.exchangerateAccesskey = definedOrElse(config.exchangerateAccesskey(), this.oem.getExchangeRateAccesskey()); - if (this.exchangerateAccesskey == null) { - this.logError(this.log, "Please configure personal Access key to access Exchange rate host API"); - return; - } this.config = config; // React on updates to Currency. @@ -127,7 +118,6 @@ private synchronized void scheduleTask(long seconds) { private final Runnable task = () -> { var token = this.securityToken; - var exchangerateAccesskey = this.exchangerateAccesskey; var areaCode = this.config.biddingZone().code; var fromDate = ZonedDateTime.now().truncatedTo(ChronoUnit.HOURS); var toDate = fromDate.plusDays(1); @@ -137,17 +127,12 @@ private synchronized void scheduleTask(long seconds) { final var result = EntsoeApi.query(token, areaCode, fromDate, toDate); final var entsoeCurrency = parseCurrency(result); final var globalCurrency = this.meta.getCurrency(); - if (globalCurrency == Currency.UNDEFINED) { - throw new OpenemsException("Global Currency is UNDEFINED. Please configure it in Core.Meta component"); - } + final double exchangeRate = getExchangeRateOrElse(entsoeCurrency, globalCurrency, 1.); - final var exchangeRate = globalCurrency.name().equals(entsoeCurrency) // - ? 1. // No need to fetch exchange rate from API. - : getExchangeRate(exchangerateAccesskey, entsoeCurrency, globalCurrency); // Parse the response for the prices - this.prices.set(parsePrices(result, "PT60M", exchangeRate)); + this.prices.set(parsePrices(result, exchangeRate)); - } catch (IOException | ParserConfigurationException | SAXException | OpenemsNamedException e) { + } catch (IOException | ParserConfigurationException | SAXException e) { this.logWarn(this.log, "Unable to Update Entsoe Time-Of-Use Price: " + e.getMessage()); e.printStackTrace(); unableToUpdatePrices = true; diff --git a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Utils.java b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Utils.java index f5025debb6f..b3b3a010a5b 100644 --- a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Utils.java +++ b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Utils.java @@ -1,83 +1,80 @@ package io.openems.edge.timeofusetariff.entsoe; import static io.openems.common.utils.XmlUtils.stream; +import static io.openems.common.utils.XmlUtils.getXmlRootDocument; import static java.lang.Double.parseDouble; +import static java.lang.Integer.parseInt; import java.io.IOException; import java.io.StringReader; +import java.time.Duration; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.TreeMap; +import java.util.Comparator; +import java.util.function.Function; +import java.util.stream.Stream; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.ImmutableTable; + import io.openems.common.utils.XmlUtils; import io.openems.edge.timeofusetariff.api.TimeOfUsePrices; public class Utils { - private static final DateTimeFormatter FORMATTER_MINUTES = DateTimeFormatter.ofPattern("u-MM-dd'T'HH:mmX"); - - private static record QueryResult(ZonedDateTime start, List prices) { - protected static class Builder { - private ZonedDateTime start; - private List prices = new ArrayList<>(); - - public Builder start(ZonedDateTime start) { - this.start = start; - return this; - } - - public Builder prices(List prices) { - this.prices.addAll(prices); - return this; - } - - public TimeOfUsePrices toTimeOfUsePrices() { - var result = new TreeMap(); - var timestamp = this.start.withZoneSameInstant(ZoneId.systemDefault()); - var quarterHourIncrements = this.prices.size() * 4; - - for (int i = 0; i < quarterHourIncrements; i++) { - result.put(timestamp, this.prices.get(i / 4)); - timestamp = timestamp.plusMinutes(15); - } - return TimeOfUsePrices.from(result); - } - } - - public static Builder create() { - return new Builder(); - } - } + protected static final DateTimeFormatter FORMATTER_MINUTES = DateTimeFormatter.ofPattern("u-MM-dd'T'HH:mmX"); /** * Parses the XML response from the Entso-E API to get the Day-Ahead prices. * * @param xml The XML string to be parsed. - * @param resolution PT15M or PT60M * @param exchangeRate The exchange rate of user currency to EUR. * @return The {@link TimeOfUsePrices} * @throws ParserConfigurationException on error. * @throws SAXException on error * @throws IOException on error */ - protected static TimeOfUsePrices parsePrices(String xml, String resolution, double exchangeRate) + protected static TimeOfUsePrices parsePrices(String xml, double exchangeRate) throws ParserConfigurationException, SAXException, IOException { - var dbFactory = DocumentBuilderFactory.newInstance(); - var dBuilder = dbFactory.newDocumentBuilder(); - var is = new InputSource(new StringReader(xml)); - var doc = dBuilder.parse(is); - var root = doc.getDocumentElement(); - var result = QueryResult.create(); + var root = getXmlRootDocument(xml); + + var allPrices = parseXml(root, exchangeRate); + if (allPrices.isEmpty()) { + return TimeOfUsePrices.EMPTY_PRICES; + } + + var shortestDuration = allPrices.rowKeySet().stream() // + .sorted() // + .findFirst().get(); + + final var prices = ImmutableSortedMap.copyOf(allPrices.row(shortestDuration)); + final var minTimestamp = prices.firstKey(); + final var maxTimestamp = prices.lastKey().plus(shortestDuration); + + var result = Stream // + .iterate(minTimestamp, // + t -> t.isBefore(maxTimestamp), // + t -> t.plusMinutes(15)) // + .collect(ImmutableSortedMap.toImmutableSortedMap(// + Comparator.naturalOrder(), // + Function.identity(), // + t -> prices.floorEntry(t).getValue())); + + return TimeOfUsePrices.from(result); + } + + protected static ImmutableTable parseXml(Element root, double exchangeRate) { + var result = ImmutableTable.builder(); stream(root) // // .filter(n -> n.getNodeName() == "TimeSeries") // @@ -85,40 +82,53 @@ protected static TimeOfUsePrices parsePrices(String xml, String resolution, doub // .filter(n -> n.getNodeName() == "Period") // // Find Period with correct resolution - .filter(p -> stream(p) // - .filter(n -> n.getNodeName() == "resolution") // - .map(XmlUtils::getContentAsString) // - .anyMatch(r -> r.equals(resolution))) // .forEach(period -> { - - var start = ZonedDateTime.parse(// - stream(period) // - // - .filter(n -> n.getNodeName() == "timeInterval") // - .flatMap(XmlUtils::stream) // - // - .filter(n -> n.getNodeName() == "start") // - .map(XmlUtils::getContentAsString) // - .findFirst().get(), - FORMATTER_MINUTES).withZoneSameInstant(ZoneId.of("UTC")); - - if (result.start == null) { - // Avoiding overwriting of start due to multiple periods. - result.start(start); + try { + parsePeriod(result, period, exchangeRate); + } catch (Exception e) { + e.printStackTrace(); } + }); + return result.build(); + } - result.prices(stream(period) // - // - .filter(n -> n.getNodeName() == "Point") // - .flatMap(XmlUtils::stream) // + protected static void parsePeriod(ImmutableTable.Builder result, Node period, + double exchangeRate) throws Exception { + final var duration = Duration.parse(stream(period) // + // + .filter(n -> n.getNodeName() == "resolution") // + .map(XmlUtils::getContentAsString) // + .findFirst().get() /* "PT15M" or "PT60M" */); + final var start = ZonedDateTime.parse(// + stream(period) // + // + .filter(n -> n.getNodeName() == "timeInterval") // + .flatMap(XmlUtils::stream) // + // + .filter(n -> n.getNodeName() == "start") // + .map(XmlUtils::getContentAsString) // + .findFirst().get(), + FORMATTER_MINUTES).withZoneSameInstant(ZoneId.of("UTC")); + + stream(period) // + // + .filter(n -> n.getNodeName() == "Point") // + .forEach(point -> { + final var position = stream(point) // + // + .filter(n -> n.getNodeName() == "position") // + .map(XmlUtils::getContentAsString) // + .map(s -> parseInt(s)) // + .findFirst().get(); + final var timestamp = start.plusMinutes((position - 1) * duration.toMinutes()); + final var price = stream(point) // // .filter(n -> n.getNodeName() == "price.amount") // .map(XmlUtils::getContentAsString) // .map(s -> parseDouble(s) * exchangeRate) // - .toList()); + .findFirst().get(); + result.put(duration, timestamp, price); }); - - return result.toTimeOfUsePrices(); } /** diff --git a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/MyConfig.java b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/MyConfig.java index 1bdbe072264..c3dbb4ec27a 100644 --- a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/MyConfig.java +++ b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/MyConfig.java @@ -8,7 +8,6 @@ public class MyConfig extends AbstractComponentConfig implements Config { protected static class Builder { private String id; private String securityToken; - private String exchangerateAccesskey; private BiddingZone biddingZone; private Builder() { @@ -24,11 +23,6 @@ public Builder setSecurityToken(String securityToken) { return this; } - public Builder setExchangerateAccesskey(String exchangerateAccesskey) { - this.exchangerateAccesskey = exchangerateAccesskey; - return this; - } - public Builder setBiddingZone(BiddingZone biddingZone) { this.biddingZone = biddingZone; return this; @@ -60,11 +54,6 @@ public String securityToken() { return this.builder.securityToken; } - @Override - public String exchangerateAccesskey() { - return this.builder.exchangerateAccesskey; - } - @Override public BiddingZone biddingZone() { return this.builder.biddingZone; diff --git a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/ParserTest.java b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/ParserTest.java index 90fee5576a2..0e6b2cb1c11 100644 --- a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/ParserTest.java +++ b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/ParserTest.java @@ -4,6 +4,8 @@ import static io.openems.edge.timeofusetariff.entsoe.Utils.parsePrices; import static org.junit.Assert.assertEquals; +import java.util.Collections; + import org.junit.Test; import io.openems.edge.common.currency.Currency; @@ -540,19 +542,261 @@ public class ParserTest { """; + private static String MISSING_DATA_AND_MULTIPLE_PERIODS_XML = """ + + + b29cfa5b47e54691a8cc110df00a748b + 1 + A44 + 10X1001A1001A450 + A32 + 10X1001A1001A450 + A33 + 2024-10-23T12:43:41Z + + 2024-10-22T22:00Z + 2024-10-24T22:00Z + + + 1 + A01 + A62 + 10Y1001A1001A46L + 10Y1001A1001A46L + A01 + EUR + MWH + A03 + + + 2024-10-23T22:00Z + 2024-10-24T22:00Z + + PT60M + + 1 + -1 + + + 3 + -0.8 + + + 4 + -0.52 + + + 5 + 0 + + + 6 + 0.84 + + + 7 + 15.47 + + + 8 + 27.59 + + + 9 + 31.86 + + + 10 + 36.97 + + + 11 + 32.96 + + + 12 + 31.14 + + + 13 + 29.92 + + + 14 + 29.55 + + + 15 + 29.71 + + + 16 + 29.76 + + + 17 + 29.98 + + + 18 + 59.65 + + + 19 + 79.41 + + + 20 + 39.97 + + + 21 + 29.93 + + + 22 + 27.68 + + + 23 + 24.95 + + + 24 + 13.27 + + + + + 2 + A01 + A62 + 10Y1001A1001A46L + 10Y1001A1001A46L + A01 + EUR + MWH + A03 + + + 2024-10-22T22:00Z + 2024-10-23T22:00Z + + PT60M + + 1 + 0 + + + 2 + -0.04 + + + 3 + -0.54 + + + 4 + -0.8 + + + 5 + -0.58 + + + 6 + 0 + + + 7 + 0.48 + + + 8 + 4.96 + + + 9 + 4.9 + + + 10 + 2.15 + + + 11 + 0.92 + + + 12 + 0.01 + + + 13 + -0.01 + + + 14 + -0.06 + + + 15 + -0.11 + + + 16 + -0.01 + + + 17 + 0 + + + 18 + 0.8 + + + 19 + 0.96 + + + 20 + 0.01 + + + 21 + 0 + + + 23 + -0.09 + + + 24 + -0.81 + + + + + """; + @Test public void testParsePrices() throws Exception { var currencyExchangeValue = 1.0; - { - var prices = parsePrices(XML, "PT15M", currencyExchangeValue).asArray(); - assertEquals(109.93, prices[0], 0.001); - assertEquals(65.07, prices[prices.length - 1], 0.001); - } - { - var prices = parsePrices(XML, "PT60M", currencyExchangeValue).asArray(); - assertEquals(84.15, prices[0], 0.001); - assertEquals(86.53, prices[prices.length - 1], 0.001); - } + var prices = parsePrices(XML, currencyExchangeValue); + assertEquals(109.93, prices.getFirst(), 0.001); + var lastKey = Collections.max(prices.pricePerQuarter.keySet()); + assertEquals(65.07, prices.pricePerQuarter.get(lastKey), 0.001); + } + + @Test + public void testParsePrices2() throws Exception { + var currencyExchangeValue = 1.0; + var prices = parsePrices(MISSING_DATA_AND_MULTIPLE_PERIODS_XML, currencyExchangeValue); + assertEquals(192, prices.pricePerQuarter.size()); + var array = prices.asArray(); + assertEquals(array[96], array[97], 0.001); // Missing value check + assertEquals(array[0], 0, 0.001); // Making sure that Periods are sorted before prices are stored. } @Test diff --git a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/TouEntsoeTest.java b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/TouEntsoeTest.java index 2c3098097d2..3aa234085ab 100644 --- a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/TouEntsoeTest.java +++ b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/TouEntsoeTest.java @@ -10,8 +10,6 @@ public class TouEntsoeTest { - private static final String COMPONENT_ID = "tou0"; - @Test public void test() throws Exception { var entsoe = new TouEntsoeImpl(); @@ -21,9 +19,8 @@ public void test() throws Exception { .addReference("meta", dummyMeta) // .addReference("oem", new DummyOpenemsEdgeOem()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId("tou0") // .setSecurityToken("") // - .setExchangerateAccesskey("") // .setBiddingZone(BiddingZone.GERMANY) // .build()); } diff --git a/io.openems.edge.timeofusetariff.groupe/readme.adoc b/io.openems.edge.timeofusetariff.groupe/readme.adoc index 2d36804c53d..9c6552c4410 100644 --- a/io.openems.edge.timeofusetariff.groupe/readme.adoc +++ b/io.openems.edge.timeofusetariff.groupe/readme.adoc @@ -4,6 +4,6 @@ This implementation uses the Groupe-E platform to receive day-ahead quarterly pr Prices retrieved from Groupe-E are subsequently converted to the user's currency (defined in Core.Meta Component) using the Exchange Rates API. -For detailed information about the Exchange Rates API, please refer to: https://exchangerate.host/#/docs +For detailed information about the Exchange Rates API, please refer to: https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.timeofusetariff.groupe[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/Config.java b/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/Config.java index 4204fc86375..ea6cb86adfe 100644 --- a/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/Config.java +++ b/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/Config.java @@ -1,7 +1,6 @@ package io.openems.edge.timeofusetariff.groupe; import org.osgi.service.metatype.annotations.AttributeDefinition; -import org.osgi.service.metatype.annotations.AttributeType; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @ObjectClassDefinition(// @@ -17,9 +16,6 @@ @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") boolean enabled() default true; - - @AttributeDefinition(name = "Exchangerate.host API Access Key", description = "Access key for Exchangerate.host: Please register at https://exchangerate.host/ to get your personal access key", type = AttributeType.PASSWORD) - String exchangerateAccesskey() default ""; String webconsole_configurationFactory_nameHint() default "Time-Of-Use Tariff GroupeE [{id}]"; } diff --git a/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImpl.java b/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImpl.java index b05f68bc2c2..52d733a6a17 100644 --- a/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImpl.java +++ b/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImpl.java @@ -3,11 +3,10 @@ import static io.openems.common.utils.JsonUtils.getAsDouble; import static io.openems.common.utils.JsonUtils.getAsString; import static io.openems.common.utils.JsonUtils.parseToJsonArray; -import static io.openems.common.utils.StringUtils.definedOrElse; +import static io.openems.edge.timeofusetariff.api.utils.ExchangeRateApi.getExchangeRateOrElse; import static io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils.generateDebugLog; import static java.util.Collections.emptyMap; -import java.io.IOException; import java.time.Clock; import java.time.Duration; import java.time.ZonedDateTime; @@ -28,8 +27,6 @@ import org.slf4j.LoggerFactory; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.oem.OpenemsEdgeOem; import io.openems.common.timedata.DurationUnit; import io.openems.edge.bridge.http.api.BridgeHttp; import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; @@ -47,7 +44,6 @@ import io.openems.edge.common.meta.Meta; import io.openems.edge.timeofusetariff.api.TimeOfUsePrices; import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; -import io.openems.edge.timeofusetariff.api.utils.ExchangeRateApi; @Designate(ocd = Config.class, factory = true) @Component(// @@ -63,15 +59,11 @@ public class TimeOfUseTariffGroupeImpl extends AbstractOpenemsComponent private final Logger log = LoggerFactory.getLogger(TimeOfUseTariffGroupeImpl.class); private final AtomicReference prices = new AtomicReference<>(TimeOfUsePrices.EMPTY_PRICES); - private String exchangerateAccesskey = null; @Reference private BridgeHttpFactory httpBridgeFactory; private BridgeHttp httpBridge; - @Reference - private OpenemsEdgeOem oem; - @Reference private Meta meta; @@ -86,7 +78,7 @@ public TimeOfUseTariffGroupeImpl() { } private final BiConsumer, Value> onCurrencyChange = (a, b) -> { - this.httpBridge = this.httpBridgeFactory.get(); + this.httpBridge.removeAllTimeEndpoints(); this.httpBridge.subscribeTime(new GroupeDelayTimeProvider(this.componentManager.getClock()), // this::createGroupeEndpoint, // this::handleEndpointResponse, // @@ -101,12 +93,6 @@ private void activate(ComponentContext context, Config config) { return; } - this.exchangerateAccesskey = definedOrElse(config.exchangerateAccesskey(), this.oem.getExchangeRateAccesskey()); - if (this.exchangerateAccesskey == null) { - this.logError(this.log, "Please configure personal Access key to access Exchange rate host API"); - return; - } - // React on updates to Currency. this.meta.getCurrencyChannel().onChange(this.onCurrencyChange); @@ -167,19 +153,12 @@ public Delay onSuccessRunDelay(HttpResponse result) { } } - private void handleEndpointResponse(HttpResponse response) throws OpenemsNamedException, IOException { + private void handleEndpointResponse(HttpResponse response) throws OpenemsNamedException { this.channel(TimeOfUseTariffGroupe.ChannelId.HTTP_STATUS_CODE).setNextValue(response.status().code()); final var groupeCurrency = Currency.CHF.name(); // Swiss Franc final var globalCurrency = this.meta.getCurrency(); - final var exchangerateAccesskey = this.exchangerateAccesskey; - if (globalCurrency == Currency.UNDEFINED) { - throw new OpenemsException("Global Currency is UNDEFINED. Please configure it in Core.Meta component"); - } - - final var exchangeRate = globalCurrency.name().equals(groupeCurrency) // - ? 1. // No need to fetch exchange rate from API. - : ExchangeRateApi.getExchangeRate(exchangerateAccesskey, groupeCurrency, globalCurrency); + final double exchangeRate = getExchangeRateOrElse(groupeCurrency, globalCurrency, 1.); // Parse the response for the prices this.prices.set(parsePrices(response.data(), exchangeRate)); diff --git a/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/MyConfig.java b/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/MyConfig.java index 13fa3e31c71..6fcaa6cc93c 100644 --- a/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/MyConfig.java +++ b/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/MyConfig.java @@ -7,7 +7,6 @@ public class MyConfig extends AbstractComponentConfig implements Config { public static class Builder { private String id; - private String exchangerateAccesskey; private Builder() { } @@ -17,11 +16,6 @@ public Builder setId(String id) { return this; } - public Builder setExchangerateAccesskey(String exchangerateAccesskey) { - this.exchangerateAccesskey = exchangerateAccesskey; - return this; - } - public MyConfig build() { return new MyConfig(this); } @@ -43,9 +37,4 @@ private MyConfig(Builder builder) { this.builder = builder; } - @Override - public String exchangerateAccesskey() { - return this.builder.exchangerateAccesskey; - } - } \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImplTest.java b/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImplTest.java index 8bdb01e4e1a..ebd8707bfb8 100644 --- a/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImplTest.java +++ b/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImplTest.java @@ -1,19 +1,15 @@ package io.openems.edge.timeofusetariff.groupe; import static io.openems.edge.common.currency.Currency.CHF; +import static io.openems.edge.common.test.TestUtils.createDummyClock; import static io.openems.edge.timeofusetariff.groupe.TimeOfUseTariffGroupeImpl.parsePrices; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; -import java.time.Instant; -import java.time.ZoneOffset; - import org.junit.Test; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.oem.DummyOpenemsEdgeOem; -import io.openems.common.test.TimeLeapClock; import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; @@ -21,7 +17,6 @@ public class TimeOfUseTariffGroupeImplTest { - private static final String CTRL_ID = "ctrl0"; private static final double GROUPE_E_EXCHANGE_RATE = 1; private static final String PRICE_RESULT_STRING = """ @@ -407,19 +402,16 @@ public class TimeOfUseTariffGroupeImplTest { @Test public void test() throws Exception { - final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); - final DummyComponentManager cm = new DummyComponentManager(clock); + final var clock = createDummyClock(); var groupe = new TimeOfUseTariffGroupeImpl(); var dummyMeta = new DummyMeta("foo0") // .withCurrency(CHF); new ComponentTest(groupe) // .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // .addReference("meta", dummyMeta) // - .addReference("oem", new DummyOpenemsEdgeOem()) // - .addReference("componentManager", cm) // + .addReference("componentManager", new DummyComponentManager(clock)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setExchangerateAccesskey("") // + .setId("ctrl0") // .build()) // ; } diff --git a/io.openems.edge.timeofusetariff.hassfurt/test/io/openems/edge/timeofusetariff/hassfurt/TimeOfUseTariffHassfurtImplTest.java b/io.openems.edge.timeofusetariff.hassfurt/test/io/openems/edge/timeofusetariff/hassfurt/TimeOfUseTariffHassfurtImplTest.java index 2f363b27876..041b9e6ae0e 100644 --- a/io.openems.edge.timeofusetariff.hassfurt/test/io/openems/edge/timeofusetariff/hassfurt/TimeOfUseTariffHassfurtImplTest.java +++ b/io.openems.edge.timeofusetariff.hassfurt/test/io/openems/edge/timeofusetariff/hassfurt/TimeOfUseTariffHassfurtImplTest.java @@ -1,24 +1,20 @@ package io.openems.edge.timeofusetariff.hassfurt; +import static io.openems.edge.common.test.TestUtils.createDummyClock; import static io.openems.edge.timeofusetariff.hassfurt.TimeOfUseTariffHassfurtImpl.parsePrices; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; -import java.time.Instant; -import java.time.ZoneOffset; - import org.junit.Test; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.test.TimeLeapClock; import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; public class TimeOfUseTariffHassfurtImplTest { - private static final String CTRL_ID = "ctrl0"; private static final String STROM_FLEX_PRO_STRING = """ { "object": "list", @@ -192,14 +188,12 @@ public class TimeOfUseTariffHassfurtImplTest { @Test public void test() throws Exception { - final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); - final DummyComponentManager cm = new DummyComponentManager(clock); - var hassfurt = new TimeOfUseTariffHassfurtImpl(); - new ComponentTest(hassfurt) // + final var clock = createDummyClock(); + new ComponentTest(new TimeOfUseTariffHassfurtImpl()) // .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // - .addReference("componentManager", cm) // + .addReference("componentManager", new DummyComponentManager(clock)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setTariffType(TariffType.STROM_FLEX) // .build()) // ; diff --git a/io.openems.edge.timeofusetariff.rabotcharge/test/io/openems/edge/timeofusetariff/rabotcharge/TimeOfUseTariffRabotChargeImplTest.java b/io.openems.edge.timeofusetariff.rabotcharge/test/io/openems/edge/timeofusetariff/rabotcharge/TimeOfUseTariffRabotChargeImplTest.java index 46c41dec126..4852a20c2d0 100644 --- a/io.openems.edge.timeofusetariff.rabotcharge/test/io/openems/edge/timeofusetariff/rabotcharge/TimeOfUseTariffRabotChargeImplTest.java +++ b/io.openems.edge.timeofusetariff.rabotcharge/test/io/openems/edge/timeofusetariff/rabotcharge/TimeOfUseTariffRabotChargeImplTest.java @@ -1,35 +1,28 @@ package io.openems.edge.timeofusetariff.rabotcharge; +import static io.openems.edge.common.test.TestUtils.createDummyClock; import static io.openems.edge.timeofusetariff.rabotcharge.TimeOfUseTariffRabotChargeImpl.parsePrices; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; -import java.time.Instant; -import java.time.ZoneOffset; - import org.junit.Test; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.test.TimeLeapClock; import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; public class TimeOfUseTariffRabotChargeImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { - final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); - final DummyComponentManager cm = new DummyComponentManager(clock); - var rabotCharge = new TimeOfUseTariffRabotChargeImpl(); - new ComponentTest(rabotCharge) // + final var clock = createDummyClock(); + new ComponentTest(new TimeOfUseTariffRabotChargeImpl()) // .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // - .addReference("componentManager", cm) // + .addReference("componentManager", new DummyComponentManager(clock)) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setAccessToken("foo-bar") // .build()) // ; diff --git a/io.openems.edge.timeofusetariff.swisspower/.classpath b/io.openems.edge.timeofusetariff.swisspower/.classpath new file mode 100644 index 00000000000..bbfbdbe40e7 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.timeofusetariff.swisspower/.gitignore b/io.openems.edge.timeofusetariff.swisspower/.gitignore new file mode 100644 index 00000000000..c2b941a96de --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/.gitignore @@ -0,0 +1,2 @@ +/bin_test/ +/generated/ diff --git a/io.openems.edge.timeofusetariff.swisspower/.project b/io.openems.edge.timeofusetariff.swisspower/.project new file mode 100644 index 00000000000..f72a46f1fa2 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.timeofusetariff.swisspower + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.timeofusetariff.swisspower/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.timeofusetariff.swisspower/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/io.openems.edge.timeofusetariff.swisspower/bnd.bnd b/io.openems.edge.timeofusetariff.swisspower/bnd.bnd new file mode 100644 index 00000000000..5c1ca041564 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/bnd.bnd @@ -0,0 +1,14 @@ +Bundle-Name: OpenEMS Edge Time-Of-Use Tariff Swisspower +Bundle-Vendor: FENECON GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 1.0.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + io.openems.common,\ + io.openems.edge.bridge.http,\ + io.openems.edge.common,\ + io.openems.edge.timeofusetariff.api,\ + +-testpath: \ + ${testpath} \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.swisspower/readme.adoc b/io.openems.edge.timeofusetariff.swisspower/readme.adoc new file mode 100644 index 00000000000..c470cec24ab --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/readme.adoc @@ -0,0 +1,11 @@ += Time-Of-Use Tariff Swisspower + +Retrieves the quarterly prices from the Swisspower ESIT API. + +For detailed information about the Swisspower ESIT API, please refer to: https://esit-test.code-fabrik.ch/doc_scalar/ + +Prices retrieved from Swisspower are subsequently converted to the user's currency (defined in Core.Meta Component) using the Exchange Rates API. + +For detailed information about the Exchange Rates API, please refer to: https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html + +https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.timeofusetariff.swisspower[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/Config.java b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/Config.java new file mode 100644 index 00000000000..bb805a5543f --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/Config.java @@ -0,0 +1,28 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.AttributeType; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "Time-Of-Use Tariff Swisspower", // + description = "Time-Of-Use Tariff implementation for Swisspower.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "timeOfUseTariff0"; + + @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") + String alias() default ""; + + @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") + boolean enabled() default true; + + @AttributeDefinition(name = "Access Token", description = "Access token for the Swisspower Platform", type = AttributeType.PASSWORD) + String accessToken() default ""; + + @AttributeDefinition(name = "Measuring point number", description = "Measuring point number for which the tariff is to be retrieved. If this option is used, the tariff name is automatically selected.") + String meteringCode() default ""; + + String webconsole_configurationFactory_nameHint() default "Time-Of-Use Tariff Swisspower [{id}]"; +} diff --git a/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspower.java b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspower.java new file mode 100644 index 00000000000..f718d966da5 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspower.java @@ -0,0 +1,37 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import io.openems.common.channel.Level; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; + +public interface TimeOfUseTariffSwisspower extends TimeOfUseTariff, OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + HTTP_STATUS_CODE(Doc.of(OpenemsType.INTEGER)// + .text("Displays the HTTP status code")), // + STATUS_INVALID_FIELDS(Doc.of(Level.WARNING) // + .text("Unable to update prices: please check your access token and metering code")), // + /** + * Should never happen. Only happens if the request has missing fields or wrong + * format of timestamps. + */ + STATUS_BAD_REQUEST(Doc.of(Level.FAULT) // + .text("Unable to update prices: internal error")), // + STATUS_READ_TIMEOUT(Doc.of(Level.WARNING) // + .text("Unable to update prices: read timeout error")), // + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } +} diff --git a/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImpl.java b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImpl.java new file mode 100644 index 00000000000..920c74bf7f3 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImpl.java @@ -0,0 +1,272 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import static io.openems.common.utils.JsonUtils.getAsDouble; +import static io.openems.common.utils.JsonUtils.getAsJsonArray; +import static io.openems.common.utils.JsonUtils.getAsString; +import static io.openems.common.utils.JsonUtils.parseToJsonObject; +import static io.openems.edge.timeofusetariff.api.utils.ExchangeRateApi.getExchangeRateOrElse; +import static io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils.generateDebugLog; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.time.Clock; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +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 io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.timedata.DurationUnit; +import io.openems.edge.bridge.http.api.BridgeHttp; +import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; +import io.openems.edge.bridge.http.api.BridgeHttpFactory; +import io.openems.edge.bridge.http.api.HttpError; +import io.openems.edge.bridge.http.api.HttpMethod; +import io.openems.edge.bridge.http.api.HttpResponse; +import io.openems.edge.bridge.http.api.UrlBuilder; +import io.openems.edge.bridge.http.time.DelayTimeProvider; +import io.openems.edge.bridge.http.time.DelayTimeProviderChain; +import io.openems.edge.common.channel.value.Value; +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.currency.Currency; +import io.openems.edge.common.meta.Meta; +import io.openems.edge.timeofusetariff.api.TimeOfUsePrices; +import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "TimeOfUseTariff.Swisspower", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class TimeOfUseTariffSwisspowerImpl extends AbstractOpenemsComponent + implements TimeOfUseTariff, OpenemsComponent, TimeOfUseTariffSwisspower { + private static final UrlBuilder URL_BASE = UrlBuilder.parse("https://esit.code-fabrik.ch/api/v1/metering_code"); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + private static final int INTERNAL_ERROR = -1; // parsing, handle exception... + private static final int DEFAULT_READ_TIMEOUT = 200; + protected static final int SERVER_ERROR_CODE = 500; + protected static final int BAD_REQUEST_ERROR_CODE = 400; + + private final Logger log = LoggerFactory.getLogger(TimeOfUseTariffSwisspowerImpl.class); + private final AtomicReference prices = new AtomicReference<>(TimeOfUsePrices.EMPTY_PRICES); + private String accessToken = null; + private String meteringCode = null; + + @Reference + private BridgeHttpFactory httpBridgeFactory; + private BridgeHttp httpBridge; + + @Reference + private Meta meta; + + @Reference + private ComponentManager componentManager; + + public TimeOfUseTariffSwisspowerImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + TimeOfUseTariffSwisspower.ChannelId.values() // + ); + } + + private final BiConsumer, Value> onCurrencyChange = (a, b) -> { + this.httpBridge.removeAllTimeEndpoints(); + this.httpBridge.subscribeTime(new SwisspowerProvider(this.componentManager.getClock()), // + this::createSwisspowerEndpoint, // + this::handleEndpointResponse, // + this::handleEndpointError); + }; + + @Activate + private void activate(ComponentContext context, Config config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + + if (!config.enabled()) { + return; + } + + this.accessToken = config.accessToken(); + if (this.accessToken == null) { + this.logError(this.log, "Please configure personal Access token to access Swisspower API"); + return; + } + + this.meteringCode = config.meteringCode(); + if (this.meteringCode == null) { + this.logError(this.log, "Please configure meteringCode to access Swisspower API"); + return; + } + + // React on updates to Currency. + this.meta.getCurrencyChannel().onChange(this.onCurrencyChange); + + this.httpBridge = this.httpBridgeFactory.get(); + this.httpBridge.subscribeTime(new SwisspowerProvider(this.componentManager.getClock()), // + this::createSwisspowerEndpoint, // + this::handleEndpointResponse, // + this::handleEndpointError); + } + + private Endpoint createSwisspowerEndpoint() { + final var now = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS); + final var startTimestamp = now.format(DATE_FORMATTER); // eg. 2024-05-22T00:00:00+02:00 + final var endTimestamp = now.plusDays(1).format(DATE_FORMATTER); + + final var url = URL_BASE.withQueryParam("start_timestamp", startTimestamp) // + .withQueryParam("end_timestamp", endTimestamp) // + .withQueryParam("metering_code", this.meteringCode); + + return new Endpoint(url.toEncodedString(), // + HttpMethod.GET, // + BridgeHttp.DEFAULT_CONNECT_TIMEOUT, // + DEFAULT_READ_TIMEOUT, // + null, // + this.buildRequestHeaders()); + } + + private Map buildRequestHeaders() { + return Map.of(// + "Authorization", "Bearer " + this.accessToken // + ); + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + this.meta.getCurrencyChannel().removeOnChangeCallback(this.onCurrencyChange); + this.httpBridgeFactory.unget(this.httpBridge); + } + + public static class SwisspowerProvider implements DelayTimeProvider { + + private final Clock clock; + + public SwisspowerProvider(Clock clock) { + super(); + this.clock = clock; + } + + @Override + public Delay onFirstRunDelay() { + return Delay.of(Duration.ofMinutes(1)); + } + + @Override + public Delay onErrorRunDelay(HttpError error) { + return DelayTimeProviderChain.fixedDelay(Duration.ofHours(1))// + .plusRandomDelay(60, ChronoUnit.SECONDS) // + .getDelay(); + } + + @Override + public Delay onSuccessRunDelay(HttpResponse result) { + return DelayTimeProviderChain.fixedAtEveryFull(this.clock, DurationUnit.ofDays(1)) + .plusRandomDelay(60, ChronoUnit.SECONDS) // + .getDelay(); + } + } + + private void handleEndpointResponse(HttpResponse response) throws OpenemsNamedException, IOException { + this.setChannelValues(response.status().code(), false, false, false); + + final var swissPowerCurrency = Currency.CHF.name(); // Swiss Franc + final var globalCurrency = this.meta.getCurrency(); + final double exchangeRate = getExchangeRateOrElse(swissPowerCurrency, globalCurrency, 1.); + + // Parse the response for the prices + this.prices.set(parsePrices(response.data(), exchangeRate)); + } + + private void handleEndpointError(HttpError error) { + var httpStatusCode = (error instanceof HttpError.ResponseError re) ? re.status.code() : INTERNAL_ERROR; + var serverError = (httpStatusCode == SERVER_ERROR_CODE); + var badRequest = (httpStatusCode == BAD_REQUEST_ERROR_CODE); + var timeoutError = (error instanceof HttpError.UnknownError e + && e.getCause() instanceof SocketTimeoutException); + + this.setChannelValues(httpStatusCode, serverError, badRequest, timeoutError); + this.log.error("HTTP Error [{}]: {}", httpStatusCode, error.getMessage()); + } + + @Override + public TimeOfUsePrices getPrices() { + return TimeOfUsePrices.from(ZonedDateTime.now(this.componentManager.getClock()), this.prices.get()); + } + + /** + * Parses JSON data to extract time-of-use prices and returns a + * {@link TimeOfUsePrices} object. + * + * @param jsonData the JSON data as a {@code String} containing the + * electricity price information. + * @param exchangeRate The exchange rate of user currency to EUR. + * @return a {@link TimeOfUsePrices} object containing the parsed prices mapped + * to their respective timestamps. + * @throws OpenemsNamedException if an error occurs during the parsing of the + * JSON data. + */ + protected static TimeOfUsePrices parsePrices(String jsonData, double exchangeRate) throws OpenemsNamedException { + var result = new TreeMap(); + var data = parseToJsonObject(jsonData); + var prices = getAsJsonArray(data, "prices"); + + for (var element : prices) { + + var startTimeString = getAsString(element, "start_timestamp"); + var integrated = getAsJsonArray(element, "integrated"); + + // CHF/kWh -> Currency/MWh + // Example: 0.1 CHF/kWh * 1000 = 100 CHF/MWh. + var marketPrice = getAsDouble(integrated.get(0), "value") * 1000 * exchangeRate; + + // Convert LocalDateTime to ZonedDateTime + var startTimeStamp = ZonedDateTime.parse(startTimeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + + // Adding the values in the Map. + result.put(startTimeStamp, marketPrice); + } + return TimeOfUsePrices.from(result); + } + + /** + * This method updates the channel values in response to an HTTP request. + * + * @param httpStatusCode the HTTP status code returned from the endpoint + * response + * @param serverError a boolean indicating if a server error occurred (status + * code 500) + * @param badRequest a boolean indicating if the request was invalid (status + * code 400) + * @param timeoutError a boolean indicating if the request could not be read + * in time + */ + private void setChannelValues(int httpStatusCode, boolean serverError, boolean badRequest, boolean timeoutError) { + this.channel(TimeOfUseTariffSwisspower.ChannelId.HTTP_STATUS_CODE).setNextValue(httpStatusCode); + this.channel(TimeOfUseTariffSwisspower.ChannelId.STATUS_INVALID_FIELDS).setNextValue(serverError); + this.channel(TimeOfUseTariffSwisspower.ChannelId.STATUS_BAD_REQUEST).setNextValue(badRequest); + this.channel(TimeOfUseTariffSwisspower.ChannelId.STATUS_READ_TIMEOUT).setNextValue(timeoutError); + } + + @Override + public String debugLog() { + return generateDebugLog(this, this.meta.getCurrency()); + } +} diff --git a/io.openems.edge.timeofusetariff.swisspower/test/.gitignore b/io.openems.edge.timeofusetariff.swisspower/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/MyConfig.java b/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/MyConfig.java new file mode 100644 index 00000000000..ea1be73e72e --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/MyConfig.java @@ -0,0 +1,62 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import io.openems.common.test.AbstractComponentConfig; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + public static class Builder { + private String id; + private String accessToken; + private String meteringCode; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public Builder setMeteringCode(String meteringCode) { + this.meteringCode = meteringCode; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String accessToken() { + return this.builder.accessToken; + } + + @Override + public String meteringCode() { + return this.builder.meteringCode; + } + +} \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImplTest.java b/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImplTest.java new file mode 100644 index 00000000000..da9f93d852b --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImplTest.java @@ -0,0 +1,226 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import static io.openems.edge.common.currency.Currency.EUR; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.timeofusetariff.swisspower.TimeOfUseTariffSwisspowerImpl.parsePrices; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyMeta; + +public class TimeOfUseTariffSwisspowerImplTest { + + private static final String CTRL_ID = "ctrl0"; + private static final double GROUPE_E_EXCHANGE_RATE = 1; + + private static final String PRICE_RESULT_STRING = """ + { + "status": "ok", + "prices": [ + { + "start_timestamp": "2024-08-12T00:00:00+02:00", + "end_timestamp": "2024-08-12T00:15:00+02:00", + "integrated": [ + { + "value": 0.49249999999999994, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T00:15:00+02:00", + "end_timestamp": "2024-08-12T00:30:00+02:00", + "integrated": [ + { + "value": 0.491133, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T00:30:00+02:00", + "end_timestamp": "2024-08-12T00:45:00+02:00", + "integrated": [ + { + "value": 0.486722, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T00:45:00+02:00", + "end_timestamp": "2024-08-12T01:00:00+02:00", + "integrated": [ + { + "value": 0.478854, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T01:00:00+02:00", + "end_timestamp": "2024-08-12T01:15:00+02:00", + "integrated": [ + { + "value": 0.46720300000000003, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T01:15:00+02:00", + "end_timestamp": "2024-08-12T01:30:00+02:00", + "integrated": [ + { + "value": 0.451539, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T01:30:00+02:00", + "end_timestamp": "2024-08-12T01:45:00+02:00", + "integrated": [ + { + "value": 0.431753, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T01:45:00+02:00", + "end_timestamp": "2024-08-12T02:00:00+02:00", + "integrated": [ + { + "value": 0.407858, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T02:00:00+02:00", + "end_timestamp": "2024-08-12T02:15:00+02:00", + "integrated": [ + { + "value": 0.38, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T02:15:00+02:00", + "end_timestamp": "2024-08-12T02:30:00+02:00", + "integrated": [ + { + "value": 0.348458, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T02:30:00+02:00", + "end_timestamp": "2024-08-12T02:45:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T02:45:00+02:00", + "end_timestamp": "2024-08-12T03:00:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T03:00:00+02:00", + "end_timestamp": "2024-08-12T03:15:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T03:15:00+02:00", + "end_timestamp": "2024-08-12T03:30:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T03:30:00+02:00", + "end_timestamp": "2024-08-12T03:45:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + } + ] + } + + """; + + @Test + public void test() throws Exception { + final var clock = createDummyClock(); + var swissPower = new TimeOfUseTariffSwisspowerImpl(); + var dummyMeta = new DummyMeta("foo0") // + .withCurrency(EUR); + new ComponentTest(swissPower) // + .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // + .addReference("meta", dummyMeta) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setAccessToken("foo-bar") // + .setMeteringCode("") // + .build()) // + ; + } + + @Test + public void nonEmptyStringTest() throws OpenemsNamedException { + // Parsing with custom data + var prices = parsePrices(PRICE_RESULT_STRING, GROUPE_E_EXCHANGE_RATE); // + + // To check if the Map is not empty + assertFalse(prices.isEmpty()); + + // To check if a value is present in map. + assertEquals(492.499, prices.getFirst(), 0.001); + } + +} diff --git a/io.openems.edge.timeofusetariff.tibber/test/io/openems/edge/timeofusetariff/tibber/TimeOfUseTariffTibberImplTest.java b/io.openems.edge.timeofusetariff.tibber/test/io/openems/edge/timeofusetariff/tibber/TimeOfUseTariffTibberImplTest.java index 59f2b6a3558..7190b8a7795 100644 --- a/io.openems.edge.timeofusetariff.tibber/test/io/openems/edge/timeofusetariff/tibber/TimeOfUseTariffTibberImplTest.java +++ b/io.openems.edge.timeofusetariff.tibber/test/io/openems/edge/timeofusetariff/tibber/TimeOfUseTariffTibberImplTest.java @@ -6,14 +6,11 @@ public class TimeOfUseTariffTibberImplTest { - private static final String CTRL_ID = "ctrl0"; - @Test public void test() throws Exception { - var tibber = new TimeOfUseTariffTibberImpl(); - new ComponentTest(tibber) // + new ComponentTest(new TimeOfUseTariffTibberImpl()) // .activate(MyConfig.create() // - .setId(CTRL_ID) // + .setId("ctrl0") // .setAccessToken("foo-bar") // .setFilter("") // .build()) // diff --git a/io.openems.wrapper/bnd.bnd b/io.openems.wrapper/bnd.bnd index d024f94a589..c1d497e0a9f 100644 --- a/io.openems.wrapper/bnd.bnd +++ b/io.openems.wrapper/bnd.bnd @@ -18,6 +18,10 @@ Bundle-Description: This wraps external java libraries that do not have OSGi hea eu.chargetime.ocpp:OCPP-J;version='1.0.2',\ eu.chargetime.ocpp:common;version='1.0.2',\ eu.chargetime.ocpp:v1_6;version='1.1.0',\ + io.helins:linux-common;version='0.1.4',\ + io.helins:linux-errno;version='1.0.2',\ + io.helins:linux-i2c;version='1.0.2',\ + io.helins:linux-io;version='0.0.4',\ io.jenetics:jenetics;version='7.2.0',\ info.faljse:SDNotify;version='1.5.0',\ io.reactivex.rxjava3.rxjava;version='3.1.8',\ diff --git a/io.openems.wrapper/helins-linux-i2c.bnd b/io.openems.wrapper/helins-linux-i2c.bnd new file mode 100644 index 00000000000..1bda4835a21 --- /dev/null +++ b/io.openems.wrapper/helins-linux-i2c.bnd @@ -0,0 +1,20 @@ +Bundle-Name: Helins linux-i2c lib +Bundle-SymbolicName: io.openems.wrapper.helins-linux-i2c +Bundle-DocURL: https://github.com/helins/linux-i2c.jav +Bundle-License: https://opensource.org/license/mpl-2-0 +Bundle-Version: 1.0.2 + +Include-Resource: \ + @linux-i2c-1.0.2.jar,\ + @linux-common-0.1.4.jar,\ + @linux-errno-1.0.2.jar,\ + @linux-io-0.0.4.jar,\ + +-dsannotations: * + +-metatypeannotations: * + +Export-Package: \ + io.helins.linux.i2c,\ + +-sources: false \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..8196d1ddf2e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "openems", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/tools/docker/backend/README.md b/tools/docker/backend/README.md index 259f8b5c14d..a785852113b 100644 --- a/tools/docker/backend/README.md +++ b/tools/docker/backend/README.md @@ -78,3 +78,9 @@ ``` *for UI Image see [ui/README.md](../ui/README.md)* + +# Common Problems and Solutions +``` +ERROR: failed to solve: error from sender: context canceled +``` +When building the Docker image this error may occur because another program is accessing the project files. Try closing these programs (e.g. Eclipse IDE) and run the build command again. \ No newline at end of file diff --git a/tools/docker/edge/README.md b/tools/docker/edge/README.md index 441b234c9a4..2cce0d4d245 100644 --- a/tools/docker/edge/README.md +++ b/tools/docker/edge/README.md @@ -35,4 +35,11 @@ docker build . -t openems_edge -f tools/docker/edge/Dockerfile ``` - *for UI Image see [ui/README.md](../ui/README.md)* \ No newline at end of file + *for UI Image see [ui/README.md](../ui/README.md)* + +# Common Problems and Solutions +``` +ERROR: failed to solve: error from sender: context canceled +``` +When building the Docker image this error may occur because another program is accessing the project files. Try closing these programs (e.g. Eclipse IDE) and run the build command again. + diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json index 8a3902ecddb..9cfb0059ab9 100644 --- a/ui/.eslintrc.json +++ b/ui/.eslintrc.json @@ -20,6 +20,7 @@ "createDefaultProgram": true }, "plugins": [ + "import", "unused-imports", "@stylistic" ], @@ -27,11 +28,19 @@ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@angular-eslint/recommended", - "plugin:@angular-eslint/template/process-inline-templates" + "plugin:@angular-eslint/template/process-inline-templates", + "plugin:import/recommended" ], "rules": { "curly": "error", "unused-imports/no-unused-imports": "error", + "import/order": [ + "error", + { + "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], + "alphabetize": { "order": "asc", "caseInsensitive": true } + } + ], "@typescript-eslint/explicit-member-accessibility": [ "error", { @@ -99,7 +108,23 @@ { "allowDefaultCaseForExhaustiveSwitch": false } + ], + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.name='fdescribe']", + "message": "Using 'fdescribe' is not allowed." + }, + { + "selector": "CallExpression[callee.name='xdescribe']", + "message": "Using 'xdescribe' is not allowed." + } ] + }, + "settings": { + "import/resolver": { + "typescript": {} + } } }, { diff --git a/ui/.project b/ui/.project new file mode 100644 index 00000000000..4f185983a47 --- /dev/null +++ b/ui/.project @@ -0,0 +1,11 @@ + + + fems-ui + + + + + + + + diff --git a/ui/README.md b/ui/README.md index 96ef2800acb..e5a4c386f2d 100644 --- a/ui/README.md +++ b/ui/README.md @@ -65,7 +65,9 @@ This project was generated with [angular-cli](https://github.com/angular/angular > [!IMPORTANT] > Crucial information necessary for users to succeed. Only provide /resources/logo-dark.png and logo.png * Move the files from res(except values and xml) to ```/android/app/src/$theme/``` (```/main``` acts as default) -* Build apps: `gradlew bundle{$theme}Release` +* Build apps (execute in order): + - `NODE_ENV="{$theme}" ./node_modules/.bin/ionic cap build android -c "$theme,$theme-backend-deploy-app" --no-open;` + - `THEME="{$theme}" gradlew bundleThemeRelease` Important (if not generated, can be copied and adjusted from existing theme): - `ui\android\app\src\{$theme}\res\xml\file_paths.xml` @@ -77,6 +79,7 @@ Use `gradlew install{$theme}Release to install it on any device` - Available Tasks: `gradlew tasks` - list available devices + emulators: `$npx native-run android --list --json` +- use Android Studio for Debugging: `$ionic cap open android` ## i18n - internationalization diff --git a/ui/angular.json b/ui/angular.json index e1fba2f0688..11947ebaaeb 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -70,7 +70,8 @@ }, "styles": [ "src/themes/openems/scss/variables.scss", - "src/global.scss" + "src/global.scss", + "src/global-ion-custom.scss" ] }, "openems-backend-dev": { @@ -162,23 +163,23 @@ "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "browserTarget": "app:build:openems,openems-edge-dev" + "buildTarget": "app:build:openems,openems-edge-dev" }, "configurations": { "openems-backend-dev": { - "browserTarget": "app:build:openems,openems-backend-dev" + "buildTarget": "app:build:openems,openems-backend-dev" }, "openems-edge-dev": { - "browserTarget": "app:build:openems,openems-edge-dev" + "buildTarget": "app:build:openems,openems-edge-dev" }, "openems-backend-prod": { - "browserTarget": "app:build:openems,openems-backend-prod,prod" + "buildTarget": "app:build:openems,openems-backend-prod,prod" }, "openems-edge-prod": { - "browserTarget": "app:build:openems,openems-edge-prod,prod" + "buildTarget": "app:build:openems,openems-edge-prod,prod" }, "openems-gitpod": { - "browserTarget": "app:build:openems,openems-gitpod" + "buildTarget": "app:build:openems,openems-gitpod" } } }, diff --git a/ui/package-lock.json b/ui/package-lock.json index fca2ca8fb74..2c6560891f3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "openems-ui", - "version": "2024.10.0", + "version": "2024.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openems-ui", - "version": "2024.10.0", + "version": "2024.11.0", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "18.2.5", @@ -27,17 +27,17 @@ "@ionic-native/core": "^5.36.0", "@ionic-native/file-opener": "^5.36.0", "@ionic/angular": "^6.7.5", - "@ngx-formly/core": "^6.3.7", - "@ngx-formly/ionic": "^6.3.7", + "@ngx-formly/core": "^6.3.10", + "@ngx-formly/ionic": "^6.3.10", "@ngx-formly/schematics": "^6.3.7", "@ngx-translate/core": "^15.0.0", "@nodro7/angular-mydatepicker": "^0.14.0", "capacitor-blob-writer": "^1.1.17", "capacitor-ios-autofill-save-password": "^3.0.0", "capacitor-secure-storage-plugin": "^0.10.0", - "chart.js": "^4.4.4", + "chart.js": "^4.4.5", "chartjs-adapter-date-fns": "^3.0.0", - "chartjs-plugin-annotation": "^3.0.1", + "chartjs-plugin-annotation": "^3.1.0", "chartjs-plugin-datalabels": "^2.2.0", "chartjs-plugin-zoom": "^2.0.1", "classlist.js": "^1.1.20150312", @@ -57,7 +57,7 @@ "zone.js": "~0.14.7" }, "devDependencies": { - "@angular-devkit/build-angular": "^18.2.5", + "@angular-devkit/build-angular": "^18.2.8", "@angular-devkit/core": "18.2.5", "@angular-devkit/schematics": "18.2.5", "@angular-eslint/builder": "^18.3.1", @@ -72,7 +72,7 @@ "@capacitor/cli": "6.1.2", "@ionic/angular-toolkit": "^11.0.1", "@ionic/cli": "^7.2.0", - "@stylistic/eslint-plugin": "^2.8.0", + "@stylistic/eslint-plugin": "^2.9.0", "@types/jasmine": "~4.3.6", "@types/jasminewd2": "~2.0.13", "@types/json-schema": "^7.0.15", @@ -85,7 +85,8 @@ "@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/types": "^8.7.0", "eslint": "^8.57.0", - "eslint-plugin-import": "2.30.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.30.0", "eslint-plugin-jsdoc": "50.2.4", "eslint-plugin-prefer-arrow": "1.2.3", "eslint-plugin-unused-imports": "^4.1.4", @@ -97,7 +98,7 @@ "karma-coverage-istanbul-reporter": "~3.0.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.1.0", - "protractor": "~7.0.0", + "protractor": "^7.0.0", "ts-node": "^10.9.2", "typescript": "~5.4.5", "typescript-strict-plugin": "^2.4.4" @@ -142,17 +143,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.6.tgz", - "integrity": "sha512-u12cJZttgs5j7gICHWSmcaTCu0EFXEzKqI8nkYCwq2MtuJlAXiMQSXYuEP9OU3Go4vMAPtQh2kShyOWCX5b4EQ==", + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.11.tgz", + "integrity": "sha512-09Ln3NAdlMw/wMLgnwYU5VgWV5TPBEHolZUIvE9D8b6SFWBCowk3B3RWeAMgg7Peuf9SKwqQHBz2b1C7RTP/8g==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.6", - "@angular-devkit/build-webpack": "0.1802.6", - "@angular-devkit/core": "18.2.6", - "@angular/build": "18.2.6", + "@angular-devkit/architect": "0.1802.11", + "@angular-devkit/build-webpack": "0.1802.11", + "@angular-devkit/core": "18.2.11", + "@angular/build": "18.2.11", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -163,7 +164,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.6", + "@ngtools/webpack": "18.2.11", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -174,7 +175,7 @@ "css-loader": "7.1.2", "esbuild-wasm": "0.23.0", "fast-glob": "3.3.2", - "http-proxy-middleware": "3.0.0", + "http-proxy-middleware": "3.0.3", "https-proxy-agent": "7.0.5", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", @@ -271,13 +272,13 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { - "version": "0.1802.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.6.tgz", - "integrity": "sha512-oF7cPFdTLxeuvXkK/opSdIxZ1E4LrBbmuytQ/nCoAGOaKBWdqvwagRZ6jVhaI0Gwu48rkcV7Zhesg/ESNnROdw==", + "version": "0.1802.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.11.tgz", + "integrity": "sha512-p+XIc/j51aI83ExNdeZwvkm1F4wkuKMGUUoj0MVUUi5E6NoiMlXYm6uU8+HbRvPBzGy5+3KOiGp3Fks0UmDSAA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.6", + "@angular-devkit/core": "18.2.11", "rxjs": "7.8.1" }, "engines": { @@ -287,9 +288,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.6.tgz", - "integrity": "sha512-la4CFvs5PcRWSkQ/H7TB5cPZirFVA9GoWk5LzIk8si6VjWBJRm8b3keKJoC9LlNeABRUIR5z0ocYkyQQUhdMfg==", + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.11.tgz", + "integrity": "sha512-H9P1shRGigORWJHUY2BRa2YurT+DVminrhuaYHsbhXBRsPmgB2Dx/30YLTnC1s5XmR9QIRUCsg/d3kyT1wd5Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -315,14 +316,14 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@angular/build": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.6.tgz", - "integrity": "sha512-TQzX6Mi7uXFvmz7+OVl4Za7WawYPcx+B5Ewm6IY/DdMyB9P/Z4tbKb1LO+ynWUXYwm7avXo6XQQ4m5ArDY5F/A==", + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.11.tgz", + "integrity": "sha512-AgirvSCmqUKiDE3C0rl3JA68OkOqQWDKUvjqRHXCkhxldLVOVoeIl87+jBYK/v9gcmk+K+ju+5wbGEfu1FjhiQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/architect": "0.1802.11", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -383,6 +384,38 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/@babel/generator": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", @@ -614,6 +647,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@angular-devkit/build-angular/node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/rollup": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", @@ -655,6 +701,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } @@ -663,16 +710,17 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.6.tgz", - "integrity": "sha512-JMLcXFaitJplwZMKkqhbYirINCRD6eOPZuIGaIOVynXYGWgvJkLT9t5C2wm9HqSLtp1K7NcYG2Y7PtTVR4krnQ==", + "version": "0.1802.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.11.tgz", + "integrity": "sha512-G76rNsyn1iQk7qjyr+K4rnDzfalmEswmwXQorypSDGaHYzIDY1SZXMoP4225WMq5fJNBOJrk82FA0PSfnPE+zQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/architect": "0.1802.11", "rxjs": "7.8.1" }, "engines": { @@ -686,13 +734,13 @@ } }, "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { - "version": "0.1802.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.6.tgz", - "integrity": "sha512-oF7cPFdTLxeuvXkK/opSdIxZ1E4LrBbmuytQ/nCoAGOaKBWdqvwagRZ6jVhaI0Gwu48rkcV7Zhesg/ESNnROdw==", + "version": "0.1802.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.11.tgz", + "integrity": "sha512-p+XIc/j51aI83ExNdeZwvkm1F4wkuKMGUUoj0MVUUi5E6NoiMlXYm6uU8+HbRvPBzGy5+3KOiGp3Fks0UmDSAA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.6", + "@angular-devkit/core": "18.2.11", "rxjs": "7.8.1" }, "engines": { @@ -702,9 +750,9 @@ } }, "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.6.tgz", - "integrity": "sha512-la4CFvs5PcRWSkQ/H7TB5cPZirFVA9GoWk5LzIk8si6VjWBJRm8b3keKJoC9LlNeABRUIR5z0ocYkyQQUhdMfg==", + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.11.tgz", + "integrity": "sha512-H9P1shRGigORWJHUY2BRa2YurT+DVminrhuaYHsbhXBRsPmgB2Dx/30YLTnC1s5XmR9QIRUCsg/d3kyT1wd5Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -1139,13 +1187,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/highlight": "^7.25.7", "picocolors": "^1.0.0" }, "engines": { @@ -1153,9 +1201,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "dev": true, "license": "MIT", "engines": { @@ -1211,16 +1259,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.0", + "@babel/types": "^7.25.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" @@ -1240,29 +1288,29 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", - "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz", + "integrity": "sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -1281,18 +1329,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", - "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/traverse": "^7.25.4", + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", "semver": "^6.3.1" }, "engines": { @@ -1302,6 +1350,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1313,14 +1374,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", - "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz", + "integrity": "sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "regexpu-core": "^5.3.1", + "@babel/helper-annotate-as-pure": "^7.25.7", + "regexpu-core": "^6.1.1", "semver": "^6.3.1" }, "engines": { @@ -1330,6 +1391,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1358,44 +1432,44 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", - "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.8" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1405,22 +1479,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", - "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", "dev": true, "license": "MIT", "engines": { @@ -1428,15 +1502,15 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", - "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz", + "integrity": "sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-wrap-function": "^7.25.0", - "@babel/traverse": "^7.25.0" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-wrap-function": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1445,16 +1519,29 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", - "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/traverse": "^7.25.0" + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1464,28 +1551,28 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", - "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1505,9 +1592,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", "dev": true, "license": "MIT", "engines": { @@ -1515,9 +1602,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", "dev": true, "license": "MIT", "engines": { @@ -1525,9 +1612,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", "dev": true, "license": "MIT", "engines": { @@ -1535,15 +1622,15 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", - "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz", + "integrity": "sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1564,13 +1651,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -1580,13 +1667,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz", - "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.4" + "@babel/types": "^7.25.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -1596,14 +1683,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", - "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz", + "integrity": "sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.3" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1613,13 +1700,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", - "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.7.tgz", + "integrity": "sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1629,13 +1716,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", - "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz", + "integrity": "sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1645,15 +1732,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", - "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz", + "integrity": "sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-transform-optional-chaining": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1663,14 +1750,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", - "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz", + "integrity": "sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.0" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1761,13 +1848,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz", + "integrity": "sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1777,13 +1864,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1946,13 +2033,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", - "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz", + "integrity": "sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1999,13 +2086,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", - "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz", + "integrity": "sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2015,13 +2102,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", - "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz", + "integrity": "sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2031,14 +2118,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", - "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz", + "integrity": "sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2048,15 +2135,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", - "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz", + "integrity": "sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2066,17 +2152,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", - "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz", + "integrity": "sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.4", + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/traverse": "^7.25.7", "globals": "^11.1.0" }, "engines": { @@ -2086,15 +2172,28 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", - "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz", + "integrity": "sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/template": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/template": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2104,13 +2203,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", - "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz", + "integrity": "sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2120,14 +2219,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", - "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz", + "integrity": "sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2137,13 +2236,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", - "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz", + "integrity": "sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2153,14 +2252,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", - "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2170,14 +2269,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", - "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz", + "integrity": "sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2187,14 +2285,14 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", - "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz", + "integrity": "sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2204,14 +2302,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", - "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz", + "integrity": "sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2221,14 +2318,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", - "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz", + "integrity": "sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2238,15 +2335,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", - "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz", + "integrity": "sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.1" + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2256,14 +2353,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", - "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz", + "integrity": "sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2273,13 +2369,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", - "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz", + "integrity": "sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2289,14 +2385,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", - "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz", + "integrity": "sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2306,13 +2401,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", - "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz", + "integrity": "sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2322,14 +2417,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", - "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz", + "integrity": "sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2339,15 +2434,15 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", - "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz", + "integrity": "sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-simple-access": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2357,16 +2452,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", - "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz", + "integrity": "sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.0" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2376,14 +2471,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", - "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz", + "integrity": "sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2393,14 +2488,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", - "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2410,13 +2505,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", - "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz", + "integrity": "sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2426,14 +2521,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", - "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2443,14 +2537,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", - "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz", + "integrity": "sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2460,16 +2553,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", - "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz", + "integrity": "sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-transform-parameters": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2479,14 +2571,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", - "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz", + "integrity": "sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2496,14 +2588,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", - "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz", + "integrity": "sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2513,15 +2604,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", - "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2531,13 +2621,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", - "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz", + "integrity": "sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2547,14 +2637,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", - "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz", + "integrity": "sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2564,16 +2654,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", - "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz", + "integrity": "sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2582,14 +2671,27 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", - "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz", + "integrity": "sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2599,13 +2701,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", - "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz", + "integrity": "sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "regenerator-transform": "^0.15.2" }, "engines": { @@ -2616,13 +2718,13 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", - "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz", + "integrity": "sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2663,13 +2765,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", - "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz", + "integrity": "sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2679,14 +2781,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", - "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz", + "integrity": "sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2696,13 +2798,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", - "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz", + "integrity": "sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2712,13 +2814,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", - "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz", + "integrity": "sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2728,13 +2830,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", - "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz", + "integrity": "sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2744,13 +2846,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", - "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz", + "integrity": "sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2760,14 +2862,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", - "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz", + "integrity": "sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2777,14 +2879,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", - "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz", + "integrity": "sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2794,14 +2896,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", - "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz", + "integrity": "sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2933,13 +3035,6 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/runtime": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", @@ -2953,32 +3048,32 @@ } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", - "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.4", - "@babel/parser": "^7.25.4", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.4", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2986,31 +3081,15 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.25.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", - "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.4", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz", - "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -3145,35 +3224,6 @@ "node": ">=8" } }, - "node_modules/@capacitor/assets/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@capacitor/assets/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@capacitor/assets/node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -3253,35 +3303,6 @@ "node": ">=8" } }, - "node_modules/@capacitor/cli/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@capacitor/cli/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@capacitor/cli/node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -4770,54 +4791,25 @@ "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@ionic/cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@ionic/cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@ionic/cli/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" + "color-name": "~1.1.4" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=7.0.0" } }, - "node_modules/@ionic/cli/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "node_modules/@ionic/cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ionic/cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, "engines": { "node": ">=8" } @@ -5470,9 +5462,9 @@ } }, "node_modules/@jsonjoy.com/util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.3.0.tgz", - "integrity": "sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5684,9 +5676,9 @@ ] }, "node_modules/@ngtools/webpack": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.6.tgz", - "integrity": "sha512-7HwOPE1EOgcHnpt4brSiT8G2CcXB50G0+CbCBaKGy4LYCG3Y3mrlzF5Fup9HvMJ6Tzqd62RqzpKKYBiGUT7hxg==", + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.11.tgz", + "integrity": "sha512-iTdUGJ5O7yMm1DyCzyoMDMxBJ68emUSSXPWbQzEEdcqmtifRebn+VAq4vHN8OmtGM1mtuKeLEsbiZP8ywrw7Ug==", "dev": true, "license": "MIT", "engines": { @@ -5701,10 +5693,9 @@ } }, "node_modules/@ngx-formly/core": { - "version": "6.3.7", - "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.7.tgz", - "integrity": "sha512-To2mH09YSm3nyThABNHIameIJCPA9C+x3/JFxFtBWek+UbYeW9DYOqNHRCc7P1ToqLqNEuwrmzjB2YSA8pO9Pw==", - "license": "MIT", + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.10.tgz", + "integrity": "sha512-YWS+0ts3aeyX7iQQop3exffmKsub5ZD18EnZPuoHSCCglLRS0tB8ra3kFTaYT0dyc8mXRQyES9TulbX9+ZdRFg==", "dependencies": { "tslib": "^2.0.0" }, @@ -5714,16 +5705,15 @@ } }, "node_modules/@ngx-formly/ionic": { - "version": "6.3.7", - "resolved": "https://registry.npmjs.org/@ngx-formly/ionic/-/ionic-6.3.7.tgz", - "integrity": "sha512-j3jiv51CVNeJGY02bZgizarO24DF5AdzL5HJ7SAtJqBIxJNR++AkQZZvgMYtPYTrvhdOIiZrhkBWh0x+rfQMJQ==", - "license": "MIT", + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/@ngx-formly/ionic/-/ionic-6.3.10.tgz", + "integrity": "sha512-nxLnWTXKrzXsuALeyPEDxp3P2G1yQvq9B/2LhIWGtqeQt3/+OW9jtX9nV/oDzwA6grsnoTOg5iPTBFuIdl6YtA==", "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@ionic/angular": "^6.0.0 || ^7.0.0", - "@ngx-formly/core": "6.3.7" + "@ngx-formly/core": "6.3.10" } }, "node_modules/@ngx-formly/schematics": { @@ -5916,6 +5906,16 @@ "tslib": "^2.0.0" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@npmcli/agent": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", @@ -6238,9 +6238,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.23.0.tgz", - "integrity": "sha512-8OR+Ok3SGEMsAZispLx8jruuXw0HVF16k+ub2eNXKHDmdxL4cf9NlNpAzhlOhNyXzKDEJuFeq0nZm+XlNb1IFw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz", + "integrity": "sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==", "cpu": [ "arm" ], @@ -6252,9 +6252,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.23.0.tgz", - "integrity": "sha512-rEFtX1nP8gqmLmPZsXRMoLVNB5JBwOzIAk/XAcEPuKrPa2nPJ+DuGGpfQUR0XjRm8KjHfTZLpWbKXkA5BoFL3w==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.3.tgz", + "integrity": "sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==", "cpu": [ "arm64" ], @@ -6266,9 +6266,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.23.0.tgz", - "integrity": "sha512-ZbqlMkJRMMPeapfaU4drYHns7Q5MIxjM/QeOO62qQZGPh9XWziap+NF9fsqPHT0KzEL6HaPspC7sOwpgyA3J9g==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.3.tgz", + "integrity": "sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==", "cpu": [ "arm64" ], @@ -6280,9 +6280,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.23.0.tgz", - "integrity": "sha512-PfmgQp78xx5rBCgn2oYPQ1rQTtOaQCna0kRaBlc5w7RlA3TDGGo7m3XaptgitUZ54US9915i7KeVPHoy3/W8tA==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.3.tgz", + "integrity": "sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==", "cpu": [ "x64" ], @@ -6293,10 +6293,38 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.3.tgz", + "integrity": "sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.3.tgz", + "integrity": "sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.23.0.tgz", - "integrity": "sha512-WAeZfAAPus56eQgBioezXRRzArAjWJGjNo/M+BHZygUcs9EePIuGI1Wfc6U/Ki+tMW17FFGvhCfYnfcKPh18SA==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.3.tgz", + "integrity": "sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==", "cpu": [ "arm" ], @@ -6308,9 +6336,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.23.0.tgz", - "integrity": "sha512-v7PGcp1O5XKZxKX8phTXtmJDVpE20Ub1eF6w9iMmI3qrrPak6yR9/5eeq7ziLMrMTjppkkskXyxnmm00HdtXjA==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.3.tgz", + "integrity": "sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==", "cpu": [ "arm" ], @@ -6322,9 +6350,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.23.0.tgz", - "integrity": "sha512-nAbWsDZ9UkU6xQiXEyXBNHAKbzSAi95H3gTStJq9UGiS1v+YVXwRHcQOQEF/3CHuhX5BVhShKoeOf6Q/1M+Zhg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.3.tgz", + "integrity": "sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==", "cpu": [ "arm64" ], @@ -6336,9 +6364,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.23.0.tgz", - "integrity": "sha512-5QT/Di5FbGNPaVw8hHO1wETunwkPuZBIu6W+5GNArlKHD9fkMHy7vS8zGHJk38oObXfWdsuLMogD4sBySLJ54g==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.3.tgz", + "integrity": "sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==", "cpu": [ "arm64" ], @@ -6350,9 +6378,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.23.0.tgz", - "integrity": "sha512-Sefl6vPyn5axzCsO13r1sHLcmPuiSOrKIImnq34CBurntcJ+lkQgAaTt/9JkgGmaZJ+OkaHmAJl4Bfd0DmdtOQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.3.tgz", + "integrity": "sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==", "cpu": [ "ppc64" ], @@ -6364,9 +6392,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.23.0.tgz", - "integrity": "sha512-o4QI2KU/QbP7ZExMse6ULotdV3oJUYMrdx3rBZCgUF3ur3gJPfe8Fuasn6tia16c5kZBBw0aTmaUygad6VB/hQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.3.tgz", + "integrity": "sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==", "cpu": [ "riscv64" ], @@ -6378,9 +6406,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.23.0.tgz", - "integrity": "sha512-+bxqx+V/D4FGrpXzPGKp/SEZIZ8cIW3K7wOtcJAoCrmXvzRtmdUhYNbgd+RztLzfDEfA2WtKj5F4tcbNPuqgeg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.3.tgz", + "integrity": "sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==", "cpu": [ "s390x" ], @@ -6392,9 +6420,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.23.0.tgz", - "integrity": "sha512-I/eXsdVoCKtSgK9OwyQKPAfricWKUMNCwJKtatRYMmDo5N859tbO3UsBw5kT3dU1n6ZcM1JDzPRSGhAUkxfLxw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.3.tgz", + "integrity": "sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==", "cpu": [ "x64" ], @@ -6406,9 +6434,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.23.0.tgz", - "integrity": "sha512-4ZoDZy5ShLbbe1KPSafbFh1vbl0asTVfkABC7eWqIs01+66ncM82YJxV2VtV3YVJTqq2P8HMx3DCoRSWB/N3rw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.3.tgz", + "integrity": "sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==", "cpu": [ "x64" ], @@ -6420,9 +6448,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.23.0.tgz", - "integrity": "sha512-+5Ky8dhft4STaOEbZu3/NU4QIyYssKO+r1cD3FzuusA0vO5gso15on7qGzKdNXnc1gOrsgCqZjRw1w+zL4y4hQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.3.tgz", + "integrity": "sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==", "cpu": [ "arm64" ], @@ -6434,9 +6462,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.23.0.tgz", - "integrity": "sha512-0SPJk4cPZQhq9qA1UhIRumSE3+JJIBBjtlGl5PNC///BoaByckNZd53rOYD0glpTkYFBQSt7AkMeLVPfx65+BQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.3.tgz", + "integrity": "sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==", "cpu": [ "ia32" ], @@ -6448,9 +6476,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.23.0.tgz", - "integrity": "sha512-lqCK5GQC8fNo0+JvTSxcG7YB1UKYp8yrNLhsArlvPWN+16ovSZgoehlVHg6X0sSWPUkpjRBR5TuR12ZugowZ4g==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.3.tgz", + "integrity": "sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==", "cpu": [ "x64" ], @@ -6599,14 +6627,14 @@ } }, "node_modules/@stylistic/eslint-plugin": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.8.0.tgz", - "integrity": "sha512-Ufvk7hP+bf+pD35R/QfunF793XlSRIC7USr3/EdgduK9j13i2JjmsM0LUz3/foS+jDYp2fzyWZA9N44CPur0Ow==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.9.0.tgz", + "integrity": "sha512-OrDyFAYjBT61122MIY1a3SfEgy3YCMgt2vL4eoPmvTwDBwyQhAXurxNQznlRD/jESNfYWfID8Ej+31LljvF7Xg==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^8.4.0", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "@typescript-eslint/utils": "^8.8.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", "estraverse": "^5.3.0", "picomatch": "^4.0.2" }, @@ -6838,9 +6866,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", - "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", "dev": true, "license": "MIT", "dependencies": { @@ -7352,14 +7380,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz", - "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", + "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0" + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7513,9 +7540,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", - "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", + "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7526,14 +7553,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", - "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", + "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7555,15 +7581,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz", - "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", + "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.7.0", - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/typescript-estree": "8.7.0" + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7577,13 +7603,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", - "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", + "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/types": "8.11.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7599,7 +7624,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -7936,9 +7960,9 @@ } }, "node_modules/adm-zip": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.15.tgz", - "integrity": "sha512-jYPWSeOA8EFoZnucrKCNihqBjoEGQSU4HKgHYQgKNEQ0pQF9a/DYuo/+fAxY76k4qe75LUlLWpAM1QWcBMTOKw==", + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", "dev": true, "license": "MIT", "engines": { @@ -8437,9 +8461,9 @@ } }, "node_modules/aws4": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.1.tgz", - "integrity": "sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "dev": true, "license": "MIT" }, @@ -8824,9 +8848,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -8844,8 +8868,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -9081,9 +9105,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001653", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz", - "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "dev": true, "funding": [ { @@ -9159,10 +9183,9 @@ "license": "MIT" }, "node_modules/chart.js": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", - "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", - "license": "MIT", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", + "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -9181,10 +9204,9 @@ } }, "node_modules/chartjs-plugin-annotation": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.0.1.tgz", - "integrity": "sha512-hlIrXXKqSDgb+ZjVYHefmlZUXK8KbkCPiynSVrTb/HjTMkT62cOInaT1NTQCKtxKKOm9oHp958DY3RTAFKtkHg==", - "license": "MIT", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz", + "integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==", "peerDependencies": { "chart.js": ">=4.0.0" } @@ -9682,34 +9704,24 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", + "negotiator": "~0.6.4", "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -9727,12 +9739,15 @@ "dev": true, "license": "MIT" }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -10064,9 +10079,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -10258,6 +10273,7 @@ "version": "0.0.24", "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", "integrity": "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==", + "deprecated": "Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11603,9 +11619,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", - "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "dev": true, "license": "ISC" }, @@ -11671,9 +11687,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "dev": true, "license": "MIT", "dependencies": { @@ -11682,7 +11698,7 @@ "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -12151,6 +12167,42 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", + "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.5", + "enhanced-resolve": "^5.15.0", + "eslint-module-utils": "^2.8.1", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, "node_modules/eslint-module-utils": { "version": "2.11.1", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.1.tgz", @@ -12351,11 +12403,10 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -12591,15 +12642,14 @@ } }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12753,9 +12803,9 @@ "license": "Apache-2.0" }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12764,7 +12814,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -12796,9 +12846,9 @@ } }, "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "license": "MIT", "engines": { @@ -13648,6 +13698,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/get-uri": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", @@ -14374,23 +14437,33 @@ } }, "node_modules/http-proxy-middleware": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0.tgz", - "integrity": "sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", + "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-proxy": "^1.17.10", - "debug": "^4.3.4", + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.5" + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/http-proxy-middleware/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -14851,6 +14924,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-bun-module": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.2.1.tgz", + "integrity": "sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -14913,16 +14996,16 @@ } }, "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -14980,6 +15063,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -15105,16 +15204,13 @@ } }, "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/is-plain-object": { @@ -15277,19 +15373,16 @@ "license": "MIT" }, "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "license": "MIT", "dependencies": { - "is-inside-container": "^1.0.0" + "is-docker": "^2.0.0" }, "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/isarray": { @@ -15594,16 +15687,16 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -17216,9 +17309,9 @@ } }, "node_modules/memfs": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.12.0.tgz", - "integrity": "sha512-74wDsex5tQDSClVkeK1vtxqYCAgCoXxx+K4NSHzgU/muYVYByFqa+0RnrPO9NM6naWm1+G9JmZ0p6QHhXmeYfA==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.0.tgz", + "integrity": "sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -17650,16 +17743,6 @@ "node": ">= 6" } }, - "node_modules/minimist-options/node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -17882,9 +17965,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.0.tgz", - "integrity": "sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -18850,6 +18933,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -20620,9 +20719,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, "license": "MIT", "dependencies": { @@ -20682,16 +20781,16 @@ } }, "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -20699,28 +20798,26 @@ "node": ">=4" } }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, "node_modules/replace": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz", @@ -21006,6 +21103,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-url-loader": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", @@ -21172,9 +21279,9 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.23.0.tgz", - "integrity": "sha512-vXB4IT9/KLDrS2WRXmY22sVB2wTsTwkpxjB8Q3mnakTENcYw3FRmfdYDy/acNmls+lHmDazgrRjK/yQ6hQAtwA==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.3.tgz", + "integrity": "sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==", "dev": true, "license": "MIT", "dependencies": { @@ -21188,22 +21295,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.23.0", - "@rollup/rollup-android-arm64": "4.23.0", - "@rollup/rollup-darwin-arm64": "4.23.0", - "@rollup/rollup-darwin-x64": "4.23.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.23.0", - "@rollup/rollup-linux-arm-musleabihf": "4.23.0", - "@rollup/rollup-linux-arm64-gnu": "4.23.0", - "@rollup/rollup-linux-arm64-musl": "4.23.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.23.0", - "@rollup/rollup-linux-riscv64-gnu": "4.23.0", - "@rollup/rollup-linux-s390x-gnu": "4.23.0", - "@rollup/rollup-linux-x64-gnu": "4.23.0", - "@rollup/rollup-linux-x64-musl": "4.23.0", - "@rollup/rollup-win32-arm64-msvc": "4.23.0", - "@rollup/rollup-win32-ia32-msvc": "4.23.0", - "@rollup/rollup-win32-x64-msvc": "4.23.0", + "@rollup/rollup-android-arm-eabi": "4.24.3", + "@rollup/rollup-android-arm64": "4.24.3", + "@rollup/rollup-darwin-arm64": "4.24.3", + "@rollup/rollup-darwin-x64": "4.24.3", + "@rollup/rollup-freebsd-arm64": "4.24.3", + "@rollup/rollup-freebsd-x64": "4.24.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.3", + "@rollup/rollup-linux-arm-musleabihf": "4.24.3", + "@rollup/rollup-linux-arm64-gnu": "4.24.3", + "@rollup/rollup-linux-arm64-musl": "4.24.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.3", + "@rollup/rollup-linux-riscv64-gnu": "4.24.3", + "@rollup/rollup-linux-s390x-gnu": "4.24.3", + "@rollup/rollup-linux-x64-gnu": "4.24.3", + "@rollup/rollup-linux-x64-musl": "4.24.3", + "@rollup/rollup-win32-arm64-msvc": "4.24.3", + "@rollup/rollup-win32-ia32-msvc": "4.24.3", + "@rollup/rollup-win32-x64-msvc": "4.24.3", "fsevents": "~2.3.2" } }, @@ -22173,9 +22282,9 @@ } }, "node_modules/socket.io": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", - "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "dev": true, "license": "MIT", "dependencies": { @@ -22183,7 +22292,7 @@ "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -22283,6 +22392,7 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -23842,9 +23952,9 @@ "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, "license": "MIT", "engines": { @@ -23866,9 +23976,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, "license": "MIT", "engines": { @@ -25059,9 +25169,9 @@ } }, "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "license": "MIT", "dependencies": { @@ -25083,6 +25193,19 @@ } } }, + "node_modules/webpack-dev-server/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/webpack-dev-server/node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", diff --git a/ui/package.json b/ui/package.json index fdacf2654a7..eed48cbc0e2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "openems-ui", - "version": "2024.10.0", + "version": "2024.11.0", "license": "AGPL-3.0", "private": true, "dependencies": { @@ -22,17 +22,17 @@ "@ionic-native/core": "^5.36.0", "@ionic-native/file-opener": "^5.36.0", "@ionic/angular": "^6.7.5", - "@ngx-formly/core": "^6.3.7", - "@ngx-formly/ionic": "^6.3.7", + "@ngx-formly/core": "^6.3.10", + "@ngx-formly/ionic": "^6.3.10", "@ngx-formly/schematics": "^6.3.7", "@ngx-translate/core": "^15.0.0", "@nodro7/angular-mydatepicker": "^0.14.0", "capacitor-blob-writer": "^1.1.17", "capacitor-ios-autofill-save-password": "^3.0.0", "capacitor-secure-storage-plugin": "^0.10.0", - "chart.js": "^4.4.4", + "chart.js": "^4.4.5", "chartjs-adapter-date-fns": "^3.0.0", - "chartjs-plugin-annotation": "^3.0.1", + "chartjs-plugin-annotation": "^3.1.0", "chartjs-plugin-datalabels": "^2.2.0", "chartjs-plugin-zoom": "^2.0.1", "classlist.js": "^1.1.20150312", @@ -52,7 +52,7 @@ "zone.js": "~0.14.7" }, "devDependencies": { - "@angular-devkit/build-angular": "^18.2.5", + "@angular-devkit/build-angular": "^18.2.8", "@angular-devkit/core": "18.2.5", "@angular-devkit/schematics": "18.2.5", "@angular-eslint/builder": "^18.3.1", @@ -67,7 +67,7 @@ "@capacitor/cli": "6.1.2", "@ionic/angular-toolkit": "^11.0.1", "@ionic/cli": "^7.2.0", - "@stylistic/eslint-plugin": "^2.8.0", + "@stylistic/eslint-plugin": "^2.9.0", "@types/jasmine": "~4.3.6", "@types/jasminewd2": "~2.0.13", "@types/json-schema": "^7.0.15", @@ -80,7 +80,8 @@ "@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/types": "^8.7.0", "eslint": "^8.57.0", - "eslint-plugin-import": "2.30.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.30.0", "eslint-plugin-jsdoc": "50.2.4", "eslint-plugin-prefer-arrow": "1.2.3", "eslint-plugin-unused-imports": "^4.1.4", @@ -92,7 +93,7 @@ "karma-coverage-istanbul-reporter": "~3.0.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.1.0", - "protractor": "~7.0.0", + "protractor": "^7.0.0", "ts-node": "^10.9.2", "typescript": "~5.4.5", "typescript-strict-plugin": "^2.4.4" diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index f84be2e897f..9aa984ea31d 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -16,9 +16,9 @@ import { OverviewComponent as GridOptimizedChargeChartOverviewComponent } from " import { OverviewComponent as TimeOfUseTariffOverviewComponent } from "./edge/history/Controller/Ess/TimeOfUseTariff/overview/overview"; import { DetailsOverviewComponent as DigitalOutputDetailsOverviewComponent } from "./edge/history/Controller/Io/DigitalOutput/details/details.overview"; import { OverviewComponent as DigitalOutputChartOverviewComponent } from "./edge/history/Controller/Io/DigitalOutput/overview/overview"; +import { OverviewComponent as HeatingelementChartOverviewComponent } from "./edge/history/Controller/Io/heatingelement/overview/overview"; import { OverviewComponent as ModbusTcpApiOverviewComponent } from "./edge/history/Controller/ModbusTcpApi/overview/overview"; import { DelayedSellToGridChartOverviewComponent } from "./edge/history/delayedselltogrid/symmetricpeakshavingchartoverview/delayedselltogridchartoverview.component"; -import { HeatingelementChartOverviewComponent } from "./edge/history/heatingelement/heatingelementchartoverview/heatingelementchartoverview.component"; import { HeatPumpChartOverviewComponent } from "./edge/history/heatpump/heatpumpchartoverview/heatpumpchartoverview.component"; import { HistoryComponent as EdgeHistoryComponent } from "./edge/history/history.component"; import { HistoryDataService } from "./edge/history/historydataservice"; @@ -91,6 +91,7 @@ export const routes: Routes = [ { path: ":componentId/gridOptimizedChargeChart", component: GridOptimizedChargeChartOverviewComponent }, { path: ":componentId/heatingelementchart", component: HeatingelementChartOverviewComponent }, { path: ":componentId/heatpumpchart", component: HeatPumpChartOverviewComponent }, + { path: ":componentId/modbusTcpApi", component: ModbusTcpApiOverviewComponent }, { path: ":componentId/scheduleChart", component: TimeOfUseTariffOverviewComponent }, { path: ":componentId/symmetricpeakshavingchart", component: SymmetricPeakshavingChartOverviewComponent }, { path: ":componentId/timeslotpeakshavingchart", component: TimeslotPeakshavingChartOverviewComponent }, @@ -101,7 +102,6 @@ export const routes: Routes = [ { path: "gridchart", component: GridChartOverviewComponent }, { path: "gridchart/:componentId", component: GridDetailsOverviewComponent }, { path: "gridchart/:componentId/currentVoltage", component: CurrentAndVoltageOverviewComponent }, - { path: ":componentId/modbusTcpApi", component: ModbusTcpApiOverviewComponent }, { path: "productionchart", component: ProductionChartOverviewComponent }, { path: "productionchart/:componentId", component: DetailsOverviewComponent }, { path: "productionchart/:componentId/currentVoltage", component: CurrentAndVoltageOverviewComponent }, @@ -134,6 +134,7 @@ export const routes: Routes = [ { path: "settings/alerting", component: EdgeSettingsAlerting, canActivate: [hasEdgeRole(Role.OWNER)], data: { navbarTitleToBeTranslated: "Edge.Config.Index.alerting" } }, { path: "settings/jsonrpctest", component: JsonrpcTestComponent, data: { navbarTitle: "Jsonrpc Test" } }, { path: "settings/powerAssistant", component: PowerAssistantComponent, canActivate: [hasEdgeRole(Role.ADMIN)], data: { navbarTitle: "Power-Assistant" } }, + { path: "settings/app", data: { navbarTitle: environment.edgeShortName + "Apps" }, component: EdgeSettingsAppIndex }, ], }, diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 80c7aa7d2a0..9b1e3b8fc02 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -1,4 +1,46 @@ + + + + + + + + Menu.menu + + + + + + + + + +

    {{ user.name }}

    + +

    {{ user.id }}

    +
    + + + + + + + + Menu.overview + + + + + + + + @@ -35,47 +77,7 @@

    Index.connectionFailed

    - - - - - - - Menu.menu - - - - - - - - -

    {{ user.name }}

    - -

    {{ user.id }}

    -
    -
    -
    - - - - - - Menu.overview - - - - - -
    -
    -
    diff --git a/ui/src/app/app.service.ts b/ui/src/app/app.service.ts index e544f51838a..4d5bddec5ab 100644 --- a/ui/src/app/app.service.ts +++ b/ui/src/app/app.service.ts @@ -3,8 +3,8 @@ import { Injectable } from "@angular/core"; import { App } from "@capacitor/app"; import { Capacitor } from "@capacitor/core"; import { Directory, Encoding, Filesystem } from "@capacitor/filesystem"; -import { FileOpener } from "@ionic-native/file-opener"; import { AlertController } from "@ionic/angular"; +import { FileOpener } from "@ionic-native/file-opener"; import { TranslateService } from "@ngx-translate/core"; import { saveAs } from "file-saver-es"; import { DeviceDetectorService, DeviceInfo } from "ngx-device-detector"; diff --git a/ui/src/app/changelog/view/component/changelog.constants.ts b/ui/src/app/changelog/view/component/changelog.constants.ts index 99abdf46dd4..fe612e45548 100644 --- a/ui/src/app/changelog/view/component/changelog.constants.ts +++ b/ui/src/app/changelog/view/component/changelog.constants.ts @@ -2,7 +2,7 @@ import { Role } from "src/app/shared/type/role"; export class Changelog { - public static readonly UI_VERSION = "2024.10.0"; + public static readonly UI_VERSION = "2024.11.0"; public static product(...products: Product[]) { return products.map(product => Changelog.link(product.name, product.url)).join(", ") + ". "; diff --git a/ui/src/app/changelog/view/view.html b/ui/src/app/changelog/view/view.html index 2746d025db1..10f23e85d60 100644 --- a/ui/src/app/changelog/view/view.html +++ b/ui/src/app/changelog/view/view.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/chart/chart.ts b/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/chart/chart.ts index 88a6d60c838..4ed2a8d0982 100644 --- a/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/chart/chart.ts +++ b/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/chart/chart.ts @@ -49,7 +49,7 @@ export class GridOptimizedChargeChartComponent extends AbstractHistoryChart { borderDash: [3, 3], }, { - name: translate.instant("General.chargePower"), + name: translate.instant("General.CHARGE"), converter: () => (data["ProductionDcActualPower"] ? diff --git a/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/overview/overview.html b/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/overview/overview.html index 0c7ce394dd6..1a8f64c0c8b 100644 --- a/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/overview/overview.html +++ b/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/overview/overview.html @@ -1,6 +1,6 @@ - + diff --git a/ui/src/app/edge/history/Controller/Ess/TimeOfUseTariff/chart/chart.ts b/ui/src/app/edge/history/Controller/Ess/TimeOfUseTariff/chart/chart.ts index 6a7850fcafb..b0df0e20a67 100644 --- a/ui/src/app/edge/history/Controller/Ess/TimeOfUseTariff/chart/chart.ts +++ b/ui/src/app/edge/history/Controller/Ess/TimeOfUseTariff/chart/chart.ts @@ -3,6 +3,7 @@ import { Component, Input } from "@angular/core"; import * as Chart from "chart.js"; import { calculateResolution, ChronoUnit, Resolution } from "src/app/edge/history/shared"; import { AbstractHistoryChart } from "src/app/shared/components/chart/abstracthistorychart"; +import { ChartConstants } from "src/app/shared/components/chart/chart.constants"; import { ChartAxis, HistoryUtils, TimeOfUseTariffUtils, Utils, YAxisType } from "src/app/shared/service/utils"; import { ChannelAddress, Currency, EdgeConfig } from "src/app/shared/shared"; import { ColorUtils } from "src/app/shared/utils/color/color.utils"; @@ -165,7 +166,10 @@ export class ChartComponent extends AbstractHistoryChart { el.borderColor = ColorUtils.changeOpacityFromRGBA(el.borderColor.toString(), 1); return el; }); - + this.options.scales[ChartAxis.LEFT].ticks = { + ...this.options.scales[ChartAxis.LEFT].ticks, + ...ChartConstants.DEFAULT_Y_SCALE_OPTIONS(this.chartObject.yAxes.find(el => el.unit === YAxisType.CURRENCY), this.translate, "line", this.datasets, true).ticks, + }; this.options.scales.x["offset"] = false; this.options["animation"] = false; }); diff --git a/ui/src/app/edge/history/Controller/Io/DigitalOutput/digitalOutput.module.ts b/ui/src/app/edge/history/Controller/Io/DigitalOutput/digitalOutput.module.ts index df92f38b353..cc060d3d5a8 100644 --- a/ui/src/app/edge/history/Controller/Io/DigitalOutput/digitalOutput.module.ts +++ b/ui/src/app/edge/history/Controller/Io/DigitalOutput/digitalOutput.module.ts @@ -1,12 +1,12 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; +import { FooterNavigationModule } from "src/app/shared/components/footer/subnavigation/footerNavigation.module"; import { SharedModule } from "src/app/shared/shared.module"; import { TotalChartComponent } from "./chart/chart"; -import { FlatComponent } from "./flat/flat"; -import { OverviewComponent } from "./overview/overview"; import { ChartComponent } from "./details/chart/chart"; import { DetailsOverviewComponent } from "./details/details.overview"; -import { FooterNavigationModule } from "src/app/shared/components/footer/subnavigation/footerNavigation.module"; +import { FlatComponent } from "./flat/flat"; +import { OverviewComponent } from "./overview/overview"; @NgModule({ imports: [ diff --git a/ui/src/app/edge/history/Controller/Io/Io.module.ts b/ui/src/app/edge/history/Controller/Io/Io.module.ts index caeda2b8417..eae8ce33c7d 100644 --- a/ui/src/app/edge/history/Controller/Io/Io.module.ts +++ b/ui/src/app/edge/history/Controller/Io/Io.module.ts @@ -1,12 +1,15 @@ import { NgModule } from "@angular/core"; import { DigitalOutput } from "./DigitalOutput/digitalOutput.module"; +import { HeatingElement } from "./heatingelement/heatingelement.module"; @NgModule({ imports: [ DigitalOutput, + HeatingElement, ], exports: [ DigitalOutput, + HeatingElement, ], }) export class ControllerIo { } diff --git a/ui/src/app/edge/history/Controller/Io/heatingelement/chart/chart.ts b/ui/src/app/edge/history/Controller/Io/heatingelement/chart/chart.ts new file mode 100644 index 00000000000..4af9f3d9c88 --- /dev/null +++ b/ui/src/app/edge/history/Controller/Io/heatingelement/chart/chart.ts @@ -0,0 +1,82 @@ +// @ts-strict-ignore +import { Component } from "@angular/core"; +import { AbstractHistoryChart } from "src/app/shared/components/chart/abstracthistorychart"; +import { ChartConstants } from "src/app/shared/components/chart/chart.constants"; +import { QueryHistoricTimeseriesEnergyResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; +import { ChartAxis, HistoryUtils, Utils, YAxisType } from "src/app/shared/service/utils"; +import { ChannelAddress, EdgeConfig } from "src/app/shared/shared"; + +@Component({ + selector: "controller-io-heatingelement-chart", + templateUrl: "../../../../../../shared/components/chart/abstracthistorychart.html", +}) +export class ChartComponent extends AbstractHistoryChart { + public static getChartData(component: EdgeConfig.Component, phaseColors: string[], chartType: "line" | "bar"): HistoryUtils.ChartData { + + const input: HistoryUtils.InputChannel[] = [ + { name: component.id, powerChannel: new ChannelAddress(component.id, "Level") }, + ]; + + for (const level of [1, 2, 3]) { + input.push({ + name: component.id + level, + powerChannel: new ChannelAddress(component.id, "Level"), + energyChannel: new ChannelAddress(component.id, "Level" + level + "CumulatedTime"), + }); + } + + return { + input: input, + output: (data: HistoryUtils.ChannelData) => { + + const output: HistoryUtils.DisplayValue[] = []; + + if (chartType === "line") { + output.push({ + name: "Level", + converter: () => data[component.id].map(val => Utils.multiplySafely(val, 1000)), + color: ChartConstants.Colors.RED, + stack: 0, + }); + } + + if (chartType === "bar") { + for (const level of [1, 2, 3]) { + output.push({ + name: "Level " + level, + nameSuffix: (energyQueryResponse: QueryHistoricTimeseriesEnergyResponse) => + energyQueryResponse?.result.data[component.id + "/Level" + level + "CumulatedTime"] ?? null, + converter: () => data[component.id + level] + // TODO add logic to not have to adjust non power data manually + .map(val => Utils.multiplySafely(val, 1000)), + color: phaseColors[level % phaseColors.length], + stack: 0, + }); + } + } + + return output; + }, + tooltip: { + formatNumber: ChartConstants.NumberFormat.NO_DECIMALS, + }, + yAxes: [ + chartType === "line" + ? { + unit: YAxisType.LEVEL, + position: "left", + yAxisId: ChartAxis.LEFT, + } + : { + unit: YAxisType.TIME, + position: "left", + yAxisId: ChartAxis.LEFT, + }, + ], + }; + } + + protected override getChartData(): HistoryUtils.ChartData { + return ChartComponent.getChartData(this.component, AbstractHistoryChart.phaseColors, this.chartType); + } +} diff --git a/ui/src/app/edge/history/Controller/Io/heatingelement/flat/flat.html b/ui/src/app/edge/history/Controller/Io/heatingelement/flat/flat.html new file mode 100644 index 00000000000..923a442cb04 --- /dev/null +++ b/ui/src/app/edge/history/Controller/Io/heatingelement/flat/flat.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/ui/src/app/edge/history/Controller/Io/heatingelement/flat/flat.ts b/ui/src/app/edge/history/Controller/Io/heatingelement/flat/flat.ts new file mode 100644 index 00000000000..677d32f0737 --- /dev/null +++ b/ui/src/app/edge/history/Controller/Io/heatingelement/flat/flat.ts @@ -0,0 +1,10 @@ +import { Component } from "@angular/core"; +import { AbstractFlatWidget } from "src/app/shared/components/flat/abstract-flat-widget"; + +@Component({ + selector: "controller-io-heatingelement-widget", + templateUrl: "./flat.html", +}) +export class FlatComponent extends AbstractFlatWidget { + protected FORMAT_SECONDS_TO_DURATION = this.Converter.FORMAT_SECONDS_TO_DURATION(this.translate.currentLang); +} diff --git a/ui/src/app/edge/history/Controller/Io/heatingelement/heatingelement.module.ts b/ui/src/app/edge/history/Controller/Io/heatingelement/heatingelement.module.ts new file mode 100644 index 00000000000..99310facb87 --- /dev/null +++ b/ui/src/app/edge/history/Controller/Io/heatingelement/heatingelement.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; +import { SharedModule } from "src/app/shared/shared.module"; +import { ChartComponent } from "./chart/chart"; +import { FlatComponent } from "./flat/flat"; +import { OverviewComponent } from "./overview/overview"; + +@NgModule({ + imports: [ + BrowserModule, + SharedModule, + ], + declarations: [ + ChartComponent, + FlatComponent, + OverviewComponent, + ], + exports: [ + ChartComponent, + FlatComponent, + OverviewComponent, + ], +}) +export class HeatingElement { } diff --git a/ui/src/app/edge/history/Controller/Io/heatingelement/overview/overview.html b/ui/src/app/edge/history/Controller/Io/heatingelement/overview/overview.html new file mode 100644 index 00000000000..d834e111f28 --- /dev/null +++ b/ui/src/app/edge/history/Controller/Io/heatingelement/overview/overview.html @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/app/edge/history/Controller/Io/heatingelement/overview/overview.ts b/ui/src/app/edge/history/Controller/Io/heatingelement/overview/overview.ts new file mode 100644 index 00000000000..2e52219327a --- /dev/null +++ b/ui/src/app/edge/history/Controller/Io/heatingelement/overview/overview.ts @@ -0,0 +1,8 @@ +import { Component } from "@angular/core"; +import { AbstractHistoryChartOverview } from "src/app/shared/components/chart/abstractHistoryChartOverview"; + +@Component({ + selector: "controller-io-heatingelement-overview", + templateUrl: "./overview.html", +}) +export class OverviewComponent extends AbstractHistoryChartOverview { } diff --git a/ui/src/app/edge/history/Controller/ModbusTcpApi/modbusTcpApi.module.ts b/ui/src/app/edge/history/Controller/ModbusTcpApi/modbusTcpApi.module.ts index aef372d5a35..b0ca5fb3021 100644 --- a/ui/src/app/edge/history/Controller/ModbusTcpApi/modbusTcpApi.module.ts +++ b/ui/src/app/edge/history/Controller/ModbusTcpApi/modbusTcpApi.module.ts @@ -1,9 +1,9 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; import { SharedModule } from "src/app/shared/shared.module"; +import { ChartComponent } from "./chart/chart"; import { FlatComponent } from "./flat/flat"; import { OverviewComponent } from "./overview/overview"; -import { ChartComponent } from "./chart/chart"; @NgModule({ imports: [ diff --git a/ui/src/app/edge/history/Controller/controller.module.ts b/ui/src/app/edge/history/Controller/controller.module.ts index e009e41d838..773e4d92f97 100644 --- a/ui/src/app/edge/history/Controller/controller.module.ts +++ b/ui/src/app/edge/history/Controller/controller.module.ts @@ -1,9 +1,9 @@ import { NgModule } from "@angular/core"; -import { ControllerEss } from "./Ess/ess.module"; -import { ControllerIo } from "./Io/Io.module"; import { ChannelThreshold } from "./ChannelThreshold/channelThreshold.module"; +import { ControllerEss } from "./Ess/ess.module"; import { GridOptimizeCharge } from "./Ess/GridoptimizedCharge/gridOptimizeCharge.module"; import { TimeOfUseTariff } from "./Ess/TimeOfUseTariff/timeOfUseTariff.module"; +import { ControllerIo } from "./Io/Io.module"; import { ModbusTcpApi } from "./ModbusTcpApi/modbusTcpApi.module"; @NgModule({ diff --git a/ui/src/app/edge/history/abstracthistorychart.ts b/ui/src/app/edge/history/abstracthistorychart.ts index 0891b3f453e..84a3923f138 100644 --- a/ui/src/app/edge/history/abstracthistorychart.ts +++ b/ui/src/app/edge/history/abstracthistorychart.ts @@ -2,7 +2,7 @@ import { TranslateService } from "@ngx-translate/core"; import * as Chart from "chart.js"; import { AbstractHistoryChart as NewAbstractHistoryChart } from "src/app/shared/components/chart/abstracthistorychart"; -import { ChartConstants, XAxisType } from "src/app/shared/components/chart/chart.constants"; +import { XAxisType } from "src/app/shared/components/chart/chart.constants"; import { JsonrpcResponseError } from "src/app/shared/jsonrpc/base"; import { QueryHistoricTimeseriesDataRequest } from "src/app/shared/jsonrpc/request/queryHistoricTimeseriesDataRequest"; import { QueryHistoricTimeseriesEnergyPerPeriodRequest } from "src/app/shared/jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest"; @@ -57,9 +57,6 @@ export abstract class AbstractHistoryChart { borderColor: "rgba(128,128,0,1)", }; - private activeQueryData: string; - private debounceTimeout: any | null = null; - constructor( public readonly spinnerId: string, protected service: Service, @@ -237,19 +234,18 @@ export abstract class AbstractHistoryChart { break; } - // Only one yAxis defined - options = NewAbstractHistoryChart.getYAxisOptions(options, yAxis, this.translate, "line", locale, ChartConstants.EMPTY_DATASETS, false); - - options.scales.x["stacked"] = true; - options.scales[ChartAxis.LEFT]["stacked"] = false; - options = NewAbstractHistoryChart.applyChartTypeSpecificOptionsChanges("line", options, this.service, chartObject); - /** Overwrite default yAxisId */ this.datasets = this.datasets .map(el => { el["yAxisID"] = ChartAxis.LEFT; return el; }); + + // Only one yAxis defined + options = NewAbstractHistoryChart.getYAxisOptions(options, yAxis, this.translate, "line", locale, this.datasets, true); + options = NewAbstractHistoryChart.applyChartTypeSpecificOptionsChanges("line", options, this.service, chartObject); + options.scales[ChartAxis.LEFT]["stacked"] = false; + options.scales.x["stacked"] = true; }).then(() => { this.options = options; resolve(); @@ -271,48 +267,33 @@ export abstract class AbstractHistoryChart { const resolution = res ?? calculateResolution(this.service, fromDate, toDate).resolution; this.errorResponse = null; - - if (this.debounceTimeout) { - clearTimeout(this.debounceTimeout); - } - - this.debounceTimeout = setTimeout(() => { - const result: Promise = new Promise((resolve, reject) => { - this.service.getCurrentEdge().then(edge => { - this.service.getConfig().then(config => { - this.setLabel(config); - this.getChannelAddresses(edge, config).then(channelAddresses => { - const request = new QueryHistoricTimeseriesDataRequest(DateUtils.maxDate(fromDate, this.edge?.firstSetupProtocol), toDate, channelAddresses, resolution); - edge.sendRequest(this.service.websocket, request).then(response => { - resolve(response as QueryHistoricTimeseriesDataResponse); - this.activeQueryData = request.id; - }).catch(error => { - this.errorResponse = error; - resolve(new QueryHistoricTimeseriesDataResponse(error.id, { - timestamps: [null], data: { null: null }, - })); - }); + const result: Promise = new Promise((resolve, reject) => { + this.service.getCurrentEdge().then(edge => { + this.service.getConfig().then(config => { + this.setLabel(config); + this.getChannelAddresses(edge, config).then(channelAddresses => { + const request = new QueryHistoricTimeseriesDataRequest(DateUtils.maxDate(fromDate, this.edge?.firstSetupProtocol), toDate, channelAddresses, resolution); + edge.sendRequest(this.service.websocket, request).then(response => { + resolve(response as QueryHistoricTimeseriesDataResponse); + }).catch(error => { + this.errorResponse = error; + resolve(new QueryHistoricTimeseriesDataResponse(error.id, { + timestamps: [null], data: { null: null }, + })); }); }); }); - }).then((response) => { - if (this.activeQueryData !== response.id) { - return; - } - if (Utils.isDataEmpty(response)) { - this.loading = false; - this.service.stopSpinner(this.spinnerId); - this.initializeChart(); - } - return DateTimeUtils.normalizeTimestamps(resolution.unit, response); }); - - return result; - }, ChartConstants.REQUEST_TIMEOUT); - - return new Promise((resolve) => { - resolve(new QueryHistoricTimeseriesDataResponse("", { timestamps: [], data: {} })); + }).then((response) => { + if (Utils.isDataEmpty(response)) { + this.loading = false; + this.service.stopSpinner(this.spinnerId); + this.initializeChart(); + } + return DateTimeUtils.normalizeTimestamps(resolution.unit, response); }); + + return result; } /** @@ -363,8 +344,7 @@ export abstract class AbstractHistoryChart { * @returns the ChartOptions */ protected createDefaultChartOptions(): Chart.ChartOptions { - const options = Utils.deepCopy(DEFAULT_TIME_CHART_OPTIONS); - return options; + return Utils.deepCopy(DEFAULT_TIME_CHART_OPTIONS); } /** diff --git a/ui/src/app/edge/history/common/consumption/Consumption.ts b/ui/src/app/edge/history/common/consumption/Consumption.ts index c36c786695d..9b957fb4e03 100644 --- a/ui/src/app/edge/history/common/consumption/Consumption.ts +++ b/ui/src/app/edge/history/common/consumption/Consumption.ts @@ -1,9 +1,9 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; +import { CurrentVoltageModule } from "src/app/shared/components/edge/meter/currentVoltage/currentVoltageModule"; import { FooterNavigationModule } from "src/app/shared/components/footer/subnavigation/footerNavigation.module"; import { SharedModule } from "src/app/shared/shared.module"; -import { CurrentVoltageModule } from "src/app/shared/components/edge/meter/currentVoltage/currentVoltageModule"; import { ChartComponent } from "./chart/chart"; import { ConsumptionMeterChartDetailsComponent } from "./details/chart/consumptionMeter"; import { EvcsChartDetailsComponent } from "./details/chart/evcs"; diff --git a/ui/src/app/edge/history/common/consumption/chart/chart.ts b/ui/src/app/edge/history/common/consumption/chart/chart.ts index bc5474bde75..001e004b59e 100644 --- a/ui/src/app/edge/history/common/consumption/chart/chart.ts +++ b/ui/src/app/edge/history/common/consumption/chart/chart.ts @@ -26,9 +26,7 @@ export class ChartComponent extends AbstractHistoryChart { component.factoryId == "Evcs.Cluster.PeakShaving" || component.factoryId == "Evcs.Cluster.SelfConsumption")); - const consumptionMeters: EdgeConfig.Component[] = config.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter") - .filter(component => component.isEnabled && config.isTypeConsumptionMetered(component)); - + // TODO Since 2024.11.0 EVCS implements EletricityMeter; use DeprecatedEvcs as filter evcsComponents.forEach(component => { inputChannel.push({ name: component.id + "/ChargePower", @@ -37,6 +35,10 @@ export class ChartComponent extends AbstractHistoryChart { }); }); + const consumptionMeters: EdgeConfig.Component[] = config.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter") + .filter(component => component.isEnabled && config.isTypeConsumptionMetered(component) + && !config.getNatureIdsByFactoryId(component.factoryId).includes("io.openems.edge.evcs.api.Evcs")); + consumptionMeters.forEach(meter => { inputChannel.push({ name: meter.id + "/ActivePower", diff --git a/ui/src/app/edge/history/common/consumption/details/details.overview.html b/ui/src/app/edge/history/common/consumption/details/details.overview.html index 31819efc6fb..bf708560a45 100644 --- a/ui/src/app/edge/history/common/consumption/details/details.overview.html +++ b/ui/src/app/edge/history/common/consumption/details/details.overview.html @@ -1,4 +1,4 @@ - + diff --git a/ui/src/app/edge/history/common/consumption/flat/flat.ts b/ui/src/app/edge/history/common/consumption/flat/flat.ts index 149521db911..a862b6ad406 100644 --- a/ui/src/app/edge/history/common/consumption/flat/flat.ts +++ b/ui/src/app/edge/history/common/consumption/flat/flat.ts @@ -14,27 +14,24 @@ export class FlatComponent extends AbstractFlatWidget { protected totalOtherEnergy: number; protected override getChannelAddresses(): ChannelAddress[] { + const channels: ChannelAddress[] = [new ChannelAddress("_sum", "ConsumptionActiveEnergy")]; this.evcsComponents = this.config?.getComponentsImplementingNature("io.openems.edge.evcs.api.Evcs") .filter(component => !(component.factoryId === "Evcs.Cluster.SelfConsumption") && !(component.factoryId === "Evcs.Cluster.PeakShaving") && !component.isEnabled === false); - - this.consumptionMeterComponents = this.config?.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter") - .filter(component => component.isEnabled && this.config.isTypeConsumptionMetered(component)); - - const channels: ChannelAddress[] = [new ChannelAddress("_sum", "ConsumptionActiveEnergy")]; - this.evcsComponents.forEach((component) => { channels.push(new ChannelAddress(component.id, "ActiveConsumptionEnergy")); }); + this.consumptionMeterComponents = this.config?.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter") + .filter(component => component.isEnabled && this.config.isTypeConsumptionMetered(component) + && !this.config.getNatureIdsByFactoryId(component.factoryId).includes("io.openems.edge.evcs.api.Evcs")); this.consumptionMeterComponents.forEach((component) => { channels.push(new ChannelAddress(component.id, "ActiveProductionEnergy")); }); - return channels; } diff --git a/ui/src/app/edge/history/common/consumption/overview/overview.ts b/ui/src/app/edge/history/common/consumption/overview/overview.ts index cf75130de35..7ba779ea15c 100644 --- a/ui/src/app/edge/history/common/consumption/overview/overview.ts +++ b/ui/src/app/edge/history/common/consumption/overview/overview.ts @@ -34,7 +34,8 @@ export class OverviewComponent extends AbstractHistoryChartOverview { !component.isEnabled === false); this.consumptionMeterComponents = this.config?.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter") - .filter(component => component.isEnabled && this.config.isTypeConsumptionMetered(component)); + .filter(component => component.isEnabled && this.config.isTypeConsumptionMetered(component) + && !this.config.getNatureIdsByFactoryId(component.factoryId).includes("io.openems.edge.evcs.api.Evcs")); const sum: EdgeConfig.Component = this.config.getComponent("_sum"); sum.alias = this.translate.instant("General.TOTAL"); diff --git a/ui/src/app/edge/history/common/energy/chart/channels.spec.ts b/ui/src/app/edge/history/common/energy/chart/channels.spec.ts index 2abfaee9aed..c5c12a0f2fa 100644 --- a/ui/src/app/edge/history/common/energy/chart/channels.spec.ts +++ b/ui/src/app/edge/history/common/energy/chart/channels.spec.ts @@ -8,7 +8,7 @@ import { QueryHistoricTimeseriesEnergyResponse } from "src/app/shared/jsonrpc/re export namespace History { - export const LINE_CHART_OPTIONS = (period: string, chartType: "line" | "bar", options: { [key: string]: { scale: { min: number, max: number; } | null, ticks: { stepSize: number }; }; }): OeChartTester.Dataset.Option => ({ + export const LINE_CHART_OPTIONS = (period: string, chartType: "line" | "bar", options: { [key: string]: { scale: { min: number, max: number; } | null, ticks?: { stepSize: number }; }; }): OeChartTester.Dataset.Option => ({ type: "option", options: { "responsive": true, "maintainAspectRatio": false, "elements": { "point": { "radius": 0, "hitRadius": 0, "hoverRadius": 0 }, "line": { "stepped": false, "fill": true } }, "datasets": { "bar": {}, "line": {} }, @@ -19,7 +19,7 @@ export namespace History { "legend": { "display": true, "position": "bottom", "labels": { "color": "" }, }, "tooltip": { - "intersect": false, "mode": "index", "callbacks": {}, + "intersect": false, "mode": "index", "callbacks": {}, "enabled": true, }, "annotation": { annotations: {}, @@ -30,11 +30,14 @@ export namespace History { }, "scales": { "x": { "stacked": true, "offset": false, "type": "time", "ticks": { "source": "auto", "maxTicksLimit": 31 }, "bounds": "ticks", "adapters": { "date": { "locale": { "code": "de", "formatLong": {}, "localize": {}, "match": {}, "options": { "weekStartsOn": 1, "firstWeekContainsDate": 4 } } } }, "time": { "unit": period as TimeUnit, "displayFormats": { "datetime": "yyyy-MM-dd HH:mm:ss", "millisecond": "SSS [ms]", "second": "HH:mm:ss a", "minute": "HH:mm", "hour": "HH:00", "day": "dd", "week": "ll", "month": "MM", "quarter": "[Q]Q - YYYY", "year": "yyyy" } } }, "left": { - ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "title": { "text": "kW", "display": true, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, + "stacked": false, + ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, + "title": { "text": "kW", "display": false, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, "ticks": { ...options["left"]?.ticks, "color": "", "padding": 5, "maxTicksLimit": ChartConstants.NUMBER_OF_Y_AXIS_TICKS }, }, "right": { - ...options["right"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "max": 100, "min": 0, "type": "linear", "title": { "text": "%", "display": true, "font": { "size": 11 }, "padding": 5 }, "position": "right", "grid": { "display": false }, + "stacked": false, + ...options["right"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "type": "linear", "title": { "text": "%", "display": false, "font": { "size": 11 }, "padding": 5 }, "position": "right", "grid": { "display": false }, "ticks": { ...options["right"]?.ticks, "color": "", @@ -56,7 +59,7 @@ export namespace History { "legend": { "display": true, "position": "bottom", "labels": { "color": "" }, }, "tooltip": { - "intersect": false, "mode": "x", "callbacks": {}, + "intersect": false, "mode": "x", "callbacks": {}, "enabled": true, }, "annotation": { annotations: {}, @@ -67,7 +70,8 @@ export namespace History { }, "scales": { "x": { "stacked": true, "offset": true, "type": "time", "ticks": { "source": "auto", "maxTicksLimit": 31 }, "bounds": "ticks", "adapters": { "date": { "locale": { "code": "de", "formatLong": {}, "localize": {}, "match": {}, "options": { "weekStartsOn": 1, "firstWeekContainsDate": 4 } } } }, "time": { "unit": period as TimeUnit, "displayFormats": { "datetime": "yyyy-MM-dd HH:mm:ss", "millisecond": "SSS [ms]", "second": "HH:mm:ss a", "minute": "HH:mm", "hour": "HH:00", "day": "dd", "week": "ll", "month": "MM", "quarter": "[Q]Q - YYYY", "year": "yyyy" } } }, "left": { - ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "title": { "text": "kWh", "display": true, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, + "stacked": true, + ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "title": { "text": "kWh", "display": false, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, "ticks": { ...options["left"]?.ticks, "color": "", diff --git a/ui/src/app/edge/history/common/energy/chart/chart.spec.ts b/ui/src/app/edge/history/common/energy/chart/chart.spec.ts index cd1695a6c81..ecf8e88a444 100644 --- a/ui/src/app/edge/history/common/energy/chart/chart.spec.ts +++ b/ui/src/app/edge/history/common/energy/chart/chart.spec.ts @@ -34,7 +34,7 @@ describe("History EnergyMonitor", () => { ], labels: LABELS(History.DAY.dataChannelWithValues.result.timestamps), options: History.LINE_CHART_OPTIONS("hour", "line", { - ["right"]: { ticks: { stepSize: 20 }, scale: null }, + ["right"]: { scale: null }, }), }, }); @@ -55,7 +55,7 @@ describe("History EnergyMonitor", () => { DATA("Ladezustand", History.WEEK.dataChannelWithValues.result.data["_sum/EssSoc"]), ], labels: LABELS(History.WEEK.dataChannelWithValues.result.timestamps), - options: History.LINE_CHART_OPTIONS("day", "line", { ["right"]: { ticks: { stepSize: 20 }, scale: null } }), + options: History.LINE_CHART_OPTIONS("day", "line", { ["right"]: { scale: null } }), }, }); } diff --git a/ui/src/app/edge/history/common/energy/chart/chart.ts b/ui/src/app/edge/history/common/energy/chart/chart.ts index dff22346d79..13f6ea38376 100644 --- a/ui/src/app/edge/history/common/energy/chart/chart.ts +++ b/ui/src/app/edge/history/common/energy/chart/chart.ts @@ -101,7 +101,7 @@ export class ChartComponent extends AbstractHistoryChart { // Charge Power { - name: translate.instant("General.chargePower"), + name: translate.instant("General.CHARGE"), nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => energyValues.result.data["_sum/EssDcChargeEnergy"], converter: () => chartType === "line" // ? data["EssCharge"]?.map((value, index) => { @@ -114,7 +114,7 @@ export class ChartComponent extends AbstractHistoryChart { // Discharge Power { - name: translate.instant("General.dischargePower"), + name: translate.instant("General.DISCHARGE"), nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => energyValues.result.data["_sum/EssDcDischargeEnergy"], converter: () => { return chartType === "line" ? @@ -200,6 +200,7 @@ export class ChartComponent extends AbstractHistoryChart { displayGrid: false, }), ], + normalizeOutputData: true, }; } diff --git a/ui/src/app/edge/history/common/grid/chart/chart.spec.ts b/ui/src/app/edge/history/common/grid/chart/chart.spec.ts index 64c0455cdf2..a52ed73cafc 100644 --- a/ui/src/app/edge/history/common/grid/chart/chart.spec.ts +++ b/ui/src/app/edge/history/common/grid/chart/chart.spec.ts @@ -51,9 +51,8 @@ describe("History Grid", () => { DATA("Bezug: 0,9 kWh", [null, null, null, 0.031, 0.018, 0, 0.02, 0.016, 0.015, 0.014, 0.009, 0.02, 0.025, 0.025, 0.025, 0.021, 0.012, 0.009, 0.01, 0.011, 0.005, 0.003, 0, 0.015, 0.018, 0.023, 0, 0, 0, 0.002, 0.002, 0.003, 0.015, 0.008, 0.022, 0.027, 0.016, 0.003, 0.002, 0, 0.028, 0.027, 0.017, 0.001, 0, 0, 0, null, null, null, null, 0.011, 0.01, 0.004, 0.006, 0.007, 0.018, 0.008, 0.012, 0.009, 0.004, 0.013, 0.015, 0.012, 0, 0, 0, 0.002, 0, 0.005, 0.001, 0.03, 0.062, 0, 0, 0, 0, 0, 0, 0, 0, 0.015, 0.005, 0.004, 0.007, 0, 0, 0, 0, 0, 0, 0, 0.005, 0, 0, 0, 0, 0, 0, 0.021, 0, 0, 0, 0, 0, 0.003, 0, 0.004, 0, 0, 0.032, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), ], labels: LABELS(History.DAY.dataChannelWithValues.result.timestamps), - options: OeTester.ChartOptions.MULTI_LINE_OPTIONS("hour", "line", { - ["right"]: { scale: { max: 1, min: 0 }, ticks: { stepSize: 1 } }, - }), + options: OeTester.ChartOptions.MULTI_LINE_OPTIONS("hour", "line", {}, + ), }, }, false); } @@ -67,9 +66,7 @@ describe("History Grid", () => { DATA("Bezug: 2,4 kWh", [0, 0.011916666666666666, 0.01633333333333333, 0.00609090909090909, 0.015333333333333334, 0.011666666666666665, 0.0024166666666666664, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.02425, 0.004416666666666667, 0.0035833333333333333, 0, 0, 0, 0.04441666666666667, 0, 0.013111111111111112, 0.001, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0011666666666666668, 0, 0, 0, 0.0015833333333333333, 0.013333333333333334, 0.020416666666666666, 0.01125, 0.019727272727272725, 0.012444444444444445, 0.009583333333333334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.007666666666666667, 0, 0.0023333333333333335, 0.0125, 0.01609090909090909, 0.02016666666666667, 0.014083333333333333, 0.006363636363636363, 0.01955555555555556, 0.04841666666666666, 0.011166666666666667, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.014222222222222221, 0.00225, 0, 0.0036666666666666666, 0.032916666666666664, 0.014666666666666666, 0.0135, 0.017363636363636362, 0.013333333333333334, 0.022083333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0009166666666666666, 0, 0.0021666666666666666, 0, 0, 0, 0.0005, 0.04841666666666666, 0, 0.005555555555555556, 0.02716666666666667, 0.017333333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0023333333333333335, 0.008333333333333333, 0.003, 0.015916666666666666, 0.00325, 0, 0.004333333333333333, 0.001, 0, 0, 0.019545454545454546, 0.0017777777777777776, 0.006416666666666667, 0.017666666666666667, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0058, 0.005625, 0, 0]), ], labels: LABELS(History.WEEK.dataChannelWithValues.result.timestamps), - options: OeTester.ChartOptions.MULTI_LINE_OPTIONS("day", "line", { - ["right"]: { scale: { max: 1, min: 0 }, ticks: { stepSize: 1 } }, - }), + options: OeTester.ChartOptions.MULTI_LINE_OPTIONS("day", "line", {}), }, }, false); } @@ -83,9 +80,7 @@ describe("History Grid", () => { DATA("Bezug: 773 kWh", [16, 6, 3, 3, 5, 48, 4, null, 5, 26, 17, 62, 8, 66, 13, 21, 4, 3, 18, 27, 29, null, 118, 85, 2, null, 72, 28, 84, null]), ], labels: LABELS(History.MONTH.energyPerPeriodChannelWithValues.result.timestamps), - options: OeTester.ChartOptions.MULTI_BAR_OPTIONS("day", "bar", { - ["right"]: { scale: {}, ticks: { stepSize: 1 } }, - }), + options: OeTester.ChartOptions.MULTI_BAR_OPTIONS("day", "bar", {}), }, }, false); @@ -100,9 +95,7 @@ describe("History Grid", () => { DATA("Bezug: 23.209 kWh", [9829, 4812, 2915, 2036, 2712, 773, 94, null, null, null, null, null]), ], labels: LABELS(History.YEAR.energyPerPeriodChannelWithValues.result.timestamps), - options: OeTester.ChartOptions.MULTI_BAR_OPTIONS("month", "bar", { - ["right"]: { scale: {}, ticks: { stepSize: 1 } }, - }), + options: OeTester.ChartOptions.MULTI_BAR_OPTIONS("month", "bar", {}), }, }, false); } diff --git a/ui/src/app/edge/history/common/grid/details/details.overview.html b/ui/src/app/edge/history/common/grid/details/details.overview.html index 1bba13ab767..506e8390f44 100644 --- a/ui/src/app/edge/history/common/grid/details/details.overview.html +++ b/ui/src/app/edge/history/common/grid/details/details.overview.html @@ -1,6 +1,8 @@ - + { - const gridMeter = Object.values(this.config.components) - .find((component) => component.isEnabled && this.config.isTypeGrid(component)) ?? null; + if (!this.component) { + return; + } + const gridMeter = this.config.isTypeGrid(this.component) ?? null; if (!gridMeter) { return; } + const gridMeters = Object.values(this.config.components) + .filter((comp) => comp.isEnabled && this.config.isTypeGrid(comp)) ?? null; + + if (gridMeters?.length == 1) { + this.title = this.translate.instant("General.grid"); + } + this.navigationButtons = [ - { id: "currentVoltage", isEnabled: edge.roleIsAtLeast(Role.INSTALLER), alias: this.translate.instant("Edge.History.CURRENT_AND_VOLTAGE"), callback: () => { this.router.navigate([`../${gridMeter.id}/currentVoltage`], { relativeTo: this.route }); } }]; + { id: "currentVoltage", isEnabled: edge.roleIsAtLeast(Role.INSTALLER), alias: this.translate.instant("Edge.History.CURRENT_AND_VOLTAGE"), callback: () => { this.router.navigate(["./currentVoltage"], { relativeTo: this.route }); } }]; }); } } diff --git a/ui/src/app/edge/history/common/grid/overview/overview.html b/ui/src/app/edge/history/common/grid/overview/overview.html index 71e464fef64..51b01128edf 100644 --- a/ui/src/app/edge/history/common/grid/overview/overview.html +++ b/ui/src/app/edge/history/common/grid/overview/overview.html @@ -3,4 +3,4 @@ - + diff --git a/ui/src/app/edge/history/common/grid/overview/overview.ts b/ui/src/app/edge/history/common/grid/overview/overview.ts index b99f248ebf5..20982339233 100644 --- a/ui/src/app/edge/history/common/grid/overview/overview.ts +++ b/ui/src/app/edge/history/common/grid/overview/overview.ts @@ -2,15 +2,18 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ModalController } from "@ionic/angular"; import { TranslateService } from "@ngx-translate/core"; +import { filter, takeUntil } from "rxjs/operators"; import { AbstractHistoryChartOverview } from "src/app/shared/components/chart/abstractHistoryChartOverview"; import { NavigationOption } from "src/app/shared/components/footer/subnavigation/footerNavigation"; import { EdgeConfig, Service } from "src/app/shared/shared"; + @Component({ templateUrl: "./overview.html", }) export class OverviewComponent extends AbstractHistoryChartOverview { protected navigationButtons: NavigationOption[] = []; + protected isAllowed: boolean = false; constructor( public override service: Service, @@ -24,24 +27,23 @@ export class OverviewComponent extends AbstractHistoryChartOverview { protected override afterIsInitialized() { - const sum: EdgeConfig.Component = this.config.getComponent("_sum"); - sum.alias = this.translate.instant("General.TOTAL"); + this.service.historyPeriod.pipe(takeUntil(this.stopOnDestroy), filter(period => !!period)) + .subscribe((period) => { + this.isAllowed = period.isWeekOrDay(); + }); + const navigationButtons: EdgeConfig.Component[] = []; const gridMeters = Object.values(this.config.components) .filter((component) => component.isEnabled && this.config.isTypeGrid(component)); if (!gridMeters) { - navigationButtons.push(sum); + return; } - if (gridMeters?.length <= 1) { - navigationButtons.push(sum); - } else { - navigationButtons.push(...gridMeters); - } + navigationButtons.push(...gridMeters); this.navigationButtons = navigationButtons.map(el => ( - { id: el.id, alias: el.alias, callback: () => { this.router.navigate(["./" + el.id], { relativeTo: this.route }); } } + { id: el.id, alias: navigationButtons.length === 1 ? this.translate.instant("Edge.History.PHASE_ACCURATE") : el.alias, callback: () => { this.router.navigate(["./" + el.id], { relativeTo: this.route }); } } )); } } diff --git a/ui/src/app/edge/history/common/production/details/chart/productionMeter.spec.ts b/ui/src/app/edge/history/common/production/details/chart/productionMeter.spec.ts index d3cc8490cc1..6a47313a6ca 100644 --- a/ui/src/app/edge/history/common/production/details/chart/productionMeter.spec.ts +++ b/ui/src/app/edge/history/common/production/details/chart/productionMeter.spec.ts @@ -52,13 +52,6 @@ describe("History Production Details - productionMeters", () => { }); export function expectView(config: EdgeConfig, testContext: TestContext & { route: ActivatedRoute }, chartType: "line" | "bar", channels: OeTester.Types.Channels, view: OeChartTester.View): void { - sessionStorage.setItem("mapping to int", JSON.stringify(History.MONTH.energyPerPeriodChannelWithValues.result.data["meter0/ActiveProductionEnergy"].map(el => el != null ? Math.round(el) : null))); - sessionStorage.setItem("phase", JSON.stringify(OeChartTester - .apply(ProductionMeterChartDetailsComponent - .getChartData( - DummyConfig.convertDummyEdgeConfigToRealEdgeConfig(config), testContext.route, - testContext.translate), chartType, channels, testContext, config))); - expect(removeFunctions(OeChartTester .apply(ProductionMeterChartDetailsComponent .getChartData( diff --git a/ui/src/app/edge/history/common/production/details/details.overview.html b/ui/src/app/edge/history/common/production/details/details.overview.html index 1884e99ef81..0e69bd49bec 100644 --- a/ui/src/app/edge/history/common/production/details/details.overview.html +++ b/ui/src/app/edge/history/common/production/details/details.overview.html @@ -1,4 +1,4 @@ - diff --git a/ui/src/app/edge/history/common/production/overview/overview.ts b/ui/src/app/edge/history/common/production/overview/overview.ts index e75c1e02ea0..6f44c4fe37a 100644 --- a/ui/src/app/edge/history/common/production/overview/overview.ts +++ b/ui/src/app/edge/history/common/production/overview/overview.ts @@ -35,10 +35,8 @@ export class OverviewComponent extends AbstractHistoryChartOverview { this.config.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter") .filter(component => component.isEnabled && this.config.isProducer(component)); - const sum: EdgeConfig.Component = this.config.getComponent("_sum"); - sum.alias = this.translate.instant("General.TOTAL"); - this.navigationButtons = [sum, ...this.chargerComponents, ...this.productionMeterComponents].map(el => ( + this.navigationButtons = [...this.productionMeterComponents, ...this.chargerComponents].map(el => ( { id: el.id, alias: el.alias, callback: () => { this.router.navigate(["./" + el.id], { relativeTo: this.route }); } } )); return []; diff --git a/ui/src/app/edge/history/common/production/production.ts b/ui/src/app/edge/history/common/production/production.ts index d4b7b9ce39c..b78789fc8de 100644 --- a/ui/src/app/edge/history/common/production/production.ts +++ b/ui/src/app/edge/history/common/production/production.ts @@ -1,9 +1,9 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; +import { CurrentVoltageModule } from "src/app/shared/components/edge/meter/currentVoltage/currentVoltageModule"; import { FooterNavigationModule } from "src/app/shared/components/footer/subnavigation/footerNavigation.module"; import { SharedModule } from "src/app/shared/shared.module"; -import { CurrentVoltageModule } from "src/app/shared/components/edge/meter/currentVoltage/currentVoltageModule"; import { TotalChartComponent } from "./chart/totalChart"; import { ChargerChartDetailsComponent } from "./details/chart/charger"; import { ProductionMeterChartDetailsComponent } from "./details/chart/productionMeter"; diff --git a/ui/src/app/edge/history/delayedselltogrid/chart.component.ts b/ui/src/app/edge/history/delayedselltogrid/chart.component.ts index deb3efeec3c..00a29929efc 100644 --- a/ui/src/app/edge/history/delayedselltogrid/chart.component.ts +++ b/ui/src/app/edge/history/delayedselltogrid/chart.component.ts @@ -147,7 +147,7 @@ export class DelayedSellToGridChartComponent extends AbstractHistoryChart implem } }); datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargeData, borderDash: [10, 10], }); @@ -168,7 +168,7 @@ export class DelayedSellToGridChartComponent extends AbstractHistoryChart implem } }); datasets.push({ - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: dischargeData, borderDash: [10, 10], }); diff --git a/ui/src/app/edge/history/heatingelement/chart.component.ts b/ui/src/app/edge/history/heatingelement/chart.component.ts deleted file mode 100644 index 49dd46bf02c..00000000000 --- a/ui/src/app/edge/history/heatingelement/chart.component.ts +++ /dev/null @@ -1,126 +0,0 @@ -// @ts-strict-ignore -import { Component, Input, OnChanges, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { TranslateService } from "@ngx-translate/core"; - -import type { ChartOptions } from "chart.js"; -import { DefaultTypes } from "src/app/shared/service/defaulttypes"; -import { ChartAxis, YAxisType } from "src/app/shared/service/utils"; - -import { QueryHistoricTimeseriesDataResponse } from "../../../shared/jsonrpc/response/queryHistoricTimeseriesDataResponse"; -import { ChannelAddress, Edge, EdgeConfig, Service } from "../../../shared/shared"; -import { AbstractHistoryChart } from "../abstracthistorychart"; - -@Component({ - selector: "heatingelementChart", - templateUrl: "../abstracthistorychart.html", -}) -export class HeatingelementChartComponent extends AbstractHistoryChart implements OnInit, OnChanges, OnDestroy { - - @Input({ required: true }) public period!: DefaultTypes.HistoryPeriod; - @Input({ required: true }) public component!: EdgeConfig.Component; - - constructor( - protected override service: Service, - protected override translate: TranslateService, - private route: ActivatedRoute, - ) { - super("heatingelement-chart", service, translate); - } - - ngOnChanges() { - this.updateChart(); - } - - ngOnInit() { - this.startSpinner(); - this.setLabel(); - } - - ngOnDestroy() { - this.unsubscribeChartRefresh(); - } - - public getChartHeight(): number { - return window.innerHeight / 1.3; - } - - protected updateChart() { - this.autoSubscribeChartRefresh(); - this.startSpinner(); - this.colors = []; - this.loading = true; - this.queryHistoricTimeseriesData(this.period.from, this.period.to).then(response => { - this.service.getCurrentEdge().then(() => { - const result = (response as QueryHistoricTimeseriesDataResponse).result; - // convert labels - const labels: Date[] = []; - for (const timestamp of result.timestamps) { - labels.push(new Date(timestamp)); - } - this.labels = labels; - - // convert datasets - const datasets = []; - const level = this.component.id + "/Level"; - - if (level in result.data) { - const levelData = result.data[level].map(value => { - if (value == null) { - return null; - } else { - return value; - } - }); - datasets.push({ - label: "Level", - data: levelData, - }); - this.colors.push({ - backgroundColor: "rgba(200,0,0,0.05)", - borderColor: "rgba(200,0,0,1)", - }); - } - this.datasets = datasets; - this.loading = false; - this.stopSpinner(); - - }).catch(reason => { - console.error(reason); // TODO error message - this.initializeChart(); - return; - }); - - }).catch(reason => { - console.error(reason); // TODO error message - this.initializeChart(); - return; - }).finally(async () => { - this.formatNumber = "1.0-1"; - this.unit = YAxisType.NONE; - await this.setOptions(this.options); - this.applyControllerSpecificOptions(this.options); - }); - } - - protected getChannelAddresses(edge: Edge, config: EdgeConfig): Promise { - return new Promise((resolve) => { - const levels = new ChannelAddress(this.component.id, "Level"); - const channeladdresses = [levels]; - resolve(channeladdresses); - }); - } - - protected applyControllerSpecificOptions(options: ChartOptions) { - options.scales[ChartAxis.LEFT]["title"].text = "Level"; - options.scales[ChartAxis.LEFT]["beginAtZero"] = true; - options.scales[ChartAxis.LEFT].max = 3; - options.scales[ChartAxis.LEFT].ticks["stepSize"] = 1; - this.options = options; - } - - protected setLabel() { - this.options = this.createDefaultChartOptions(); - } - -} diff --git a/ui/src/app/edge/history/heatingelement/heatingelementchartoverview/heatingelementchartoverview.component.html b/ui/src/app/edge/history/heatingelement/heatingelementchartoverview/heatingelementchartoverview.component.html deleted file mode 100644 index 9b116e552fb..00000000000 --- a/ui/src/app/edge/history/heatingelement/heatingelementchartoverview/heatingelementchartoverview.component.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - {{ component.alias }} - - - - - - - - - - - - - - - - - - - diff --git a/ui/src/app/edge/history/heatingelement/heatingelementchartoverview/heatingelementchartoverview.component.ts b/ui/src/app/edge/history/heatingelement/heatingelementchartoverview/heatingelementchartoverview.component.ts deleted file mode 100644 index f615818d7c3..00000000000 --- a/ui/src/app/edge/history/heatingelement/heatingelementchartoverview/heatingelementchartoverview.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { Edge, EdgeConfig, Service } from "../../../../shared/shared"; - -@Component({ - selector: HeatingelementChartOverviewComponent.SELECTOR, - templateUrl: "./heatingelementchartoverview.component.html", -}) -export class HeatingelementChartOverviewComponent implements OnInit { - - private static readonly SELECTOR = "heatingelement-chart-overview"; - public edge: Edge | null = null; - public component: EdgeConfig.Component | null = null; - - constructor( - public service: Service, - private route: ActivatedRoute, - ) { } - - ngOnInit() { - this.service.getCurrentEdge().then(edge => { - this.service.getConfig().then(config => { - this.component = config.getComponent(this.route.snapshot.params.componentId); - this.service.getConfig().then(config => { - this.edge = edge; - this.component = config.getComponent(this.route.snapshot.params.componentId); - }); - }); - }); - } -} diff --git a/ui/src/app/edge/history/heatingelement/widget.component.html b/ui/src/app/edge/history/heatingelement/widget.component.html deleted file mode 100644 index 49b50bff319..00000000000 --- a/ui/src/app/edge/history/heatingelement/widget.component.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - {{ component.alias }} - - - - - - - - - - - - - - - - - -
    - Edge.History.activeDuration Level 1 - - {{ activeTimeOverPeriodLevel1 | formatSecondsToDuration }} -
    - Edge.History.activeDuration Level 2 - - {{ activeTimeOverPeriodLevel2 | formatSecondsToDuration }} -
    - Edge.History.activeDuration Level 3 - {{ activeTimeOverPeriodLevel3 | formatSecondsToDuration }} -
    -
    -
    -
    -
    diff --git a/ui/src/app/edge/history/heatingelement/widget.component.ts b/ui/src/app/edge/history/heatingelement/widget.component.ts deleted file mode 100644 index 262c025d23c..00000000000 --- a/ui/src/app/edge/history/heatingelement/widget.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Component, Input, OnChanges, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { QueryHistoricTimeseriesDataResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesDataResponse"; -import { DefaultTypes } from "src/app/shared/service/defaulttypes"; - -import { ChannelAddress, Edge, EdgeConfig, Service } from "../../../shared/shared"; -import { AbstractHistoryWidget } from "../abstracthistorywidget"; - -@Component({ - selector: HeatingelementWidgetComponent.SELECTOR, - templateUrl: "./widget.component.html", -}) -export class HeatingelementWidgetComponent extends AbstractHistoryWidget implements OnInit, OnChanges, OnDestroy { - - private static readonly SELECTOR = "heatingelementWidget"; - @Input({ required: true }) public period!: DefaultTypes.HistoryPeriod; - @Input({ required: true }) public componentId!: string; - - - public component: EdgeConfig.Component | null = null; - - public activeTimeOverPeriodLevel1: number | null = null; - public activeTimeOverPeriodLevel2: number | null = null; - public activeTimeOverPeriodLevel3: number | null = null; - - public edge: Edge | null = null; - - constructor( - public override service: Service, - private route: ActivatedRoute, - ) { - super(service); - } - - ngOnInit() { - this.service.getCurrentEdge().then(edge => { - this.edge = edge; - this.service.getConfig().then(config => { - this.component = config.getComponent(this.componentId); - }); - }); - } - - ngOnDestroy() { - this.unsubscribeWidgetRefresh(); - } - - ngOnChanges() { - this.updateValues(); - } - - public getCumulativeValue(channeladdress: string, response: QueryHistoricTimeseriesDataResponse) { - const array = response.result.data[channeladdress]; - const firstValue = array.find(el => el != null) ?? 0; - const lastValue = array.slice().reverse().find(el => el != null) ?? 0; - return lastValue - firstValue; - } - - protected updateValues() { - this.queryHistoricTimeseriesData(this.service.historyPeriod.value.from, this.service.historyPeriod.value.to).then(response => { - this.activeTimeOverPeriodLevel1 = this.getCumulativeValue(this.componentId + "/Level1CumulatedTime", response); - this.activeTimeOverPeriodLevel2 = this.getCumulativeValue(this.componentId + "/Level2CumulatedTime", response); - this.activeTimeOverPeriodLevel3 = this.getCumulativeValue(this.componentId + "/Level3CumulatedTime", response); - }); - } - - protected getChannelAddresses(edge: Edge, config: EdgeConfig): Promise { - return new Promise((resolve) => { - const channeladdresses = [ - new ChannelAddress(this.componentId, "Level1CumulatedTime"), - new ChannelAddress(this.componentId, "Level2CumulatedTime"), - new ChannelAddress(this.componentId, "Level3CumulatedTime"), - ]; - resolve(channeladdresses); - }); - } -} diff --git a/ui/src/app/edge/history/history.component.html b/ui/src/app/edge/history/history.component.html index bd86a09bf04..45655fbc05e 100644 --- a/ui/src/app/edge/history/history.component.html +++ b/ui/src/app/edge/history/history.component.html @@ -1,4 +1,3 @@ -
    @@ -46,10 +45,6 @@ - - - - @@ -89,8 +84,8 @@ - - + + diff --git a/ui/src/app/edge/history/history.component.ts b/ui/src/app/edge/history/history.component.ts index e2349530d0d..1e872a6bca5 100644 --- a/ui/src/app/edge/history/history.component.ts +++ b/ui/src/app/edge/history/history.component.ts @@ -1,9 +1,8 @@ // @ts-strict-ignore -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import { AppService } from "src/app/app.service"; -import { HeaderComponent } from "src/app/shared/components/header/header.component"; import { JsonrpcResponseError } from "src/app/shared/jsonrpc/base"; import { Edge, EdgeConfig, EdgePermission, Service, Widgets } from "src/app/shared/shared"; import { environment } from "src/environments"; @@ -14,8 +13,6 @@ import { environment } from "src/environments"; }) export class HistoryComponent implements OnInit { - @ViewChild(HeaderComponent, { static: false }) public HeaderComponent: HeaderComponent; - // is a Timedata service available, i.e. can historic data be queried. public isTimedataAvailable: boolean = true; @@ -65,12 +62,6 @@ export class HistoryComponent implements OnInit { }); } - // checks arrows when ChartPage is closed - // double viewchild is used to prevent undefined state of PickDateComponent - ionViewDidEnter() { - this.HeaderComponent.PickDateComponent.checkArrowAutomaticForwarding(); - } - updateOnWindowResize() { const ref = /* fix proportions */ Math.min(window.innerHeight - 150, /* handle grid breakpoints */(window.innerWidth < 768 ? window.innerWidth - 150 : window.innerWidth - 400)); diff --git a/ui/src/app/edge/history/history.module.ts b/ui/src/app/edge/history/history.module.ts index 9d45b5ba2df..0c0fd1d6c06 100644 --- a/ui/src/app/edge/history/history.module.ts +++ b/ui/src/app/edge/history/history.module.ts @@ -1,6 +1,5 @@ import { NgModule } from "@angular/core"; import { HistoryDataErrorModule } from "src/app/shared/components/history-data-error/history-data-error.module"; - import { SharedModule } from "../../shared/shared.module"; import { ChpSocChartComponent } from "./chpsoc/chart.component"; import { ChpSocWidgetComponent } from "./chpsoc/widget.component"; @@ -9,9 +8,6 @@ import { Controller } from "./Controller/controller.module"; import { DelayedSellToGridChartComponent } from "./delayedselltogrid/chart.component"; import { DelayedSellToGridChartOverviewComponent } from "./delayedselltogrid/symmetricpeakshavingchartoverview/delayedselltogridchartoverview.component"; import { DelayedSellToGridWidgetComponent } from "./delayedselltogrid/widget.component"; -import { HeatingelementChartComponent } from "./heatingelement/chart.component"; -import { HeatingelementChartOverviewComponent } from "./heatingelement/heatingelementchartoverview/heatingelementchartoverview.component"; -import { HeatingelementWidgetComponent } from "./heatingelement/widget.component"; import { HeatPumpChartComponent } from "./heatpump/chart.component"; import { HeatPumpChartOverviewComponent } from "./heatpump/heatpumpchartoverview/heatpumpchartoverview.component"; import { HeatpumpWidgetComponent } from "./heatpump/widget.component"; @@ -36,10 +32,10 @@ import { StorageComponent } from "./storage/widget.component"; @NgModule({ imports: [ - SharedModule, Common, Controller, HistoryDataErrorModule, + SharedModule, ], declarations: [ AsymmetricPeakshavingChartComponent, @@ -50,13 +46,11 @@ import { StorageComponent } from "./storage/widget.component"; DelayedSellToGridChartComponent, DelayedSellToGridChartOverviewComponent, DelayedSellToGridWidgetComponent, - HeatingelementChartComponent, - HeatingelementChartOverviewComponent, - HeatingelementWidgetComponent, HeatPumpChartComponent, HeatPumpChartOverviewComponent, HeatpumpWidgetComponent, HistoryComponent, + HistoryParentComponent, SocStorageChartComponent, StorageChargerChartComponent, StorageChartOverviewComponent, @@ -70,7 +64,6 @@ import { StorageComponent } from "./storage/widget.component"; TimeslotPeakshavingChartComponent, TimeslotPeakshavingChartOverviewComponent, TimeslotPeakshavingWidgetComponent, - HistoryParentComponent, ], }) export class HistoryModule { } diff --git a/ui/src/app/edge/history/peakshaving/asymmetric/chart.component.ts b/ui/src/app/edge/history/peakshaving/asymmetric/chart.component.ts index d9b4b4ae008..aeb02eda9e6 100644 --- a/ui/src/app/edge/history/peakshaving/asymmetric/chart.component.ts +++ b/ui/src/app/edge/history/peakshaving/asymmetric/chart.component.ts @@ -178,7 +178,7 @@ export class AsymmetricPeakshavingChartComponent extends AbstractHistoryChart im } }); datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargeData, }); this.colors.push({ @@ -198,7 +198,7 @@ export class AsymmetricPeakshavingChartComponent extends AbstractHistoryChart im } }); datasets.push({ - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: dischargeData, }); this.colors.push({ diff --git a/ui/src/app/edge/history/peakshaving/symmetric/chart.component.ts b/ui/src/app/edge/history/peakshaving/symmetric/chart.component.ts index 4c9d97e725f..25aa0c7cc0b 100644 --- a/ui/src/app/edge/history/peakshaving/symmetric/chart.component.ts +++ b/ui/src/app/edge/history/peakshaving/symmetric/chart.component.ts @@ -145,7 +145,7 @@ export class SymmetricPeakshavingChartComponent extends AbstractHistoryChart imp } }); datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargeData, borderDash: [10, 10], }); @@ -166,7 +166,7 @@ export class SymmetricPeakshavingChartComponent extends AbstractHistoryChart imp } }); datasets.push({ - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: dischargeData, borderDash: [10, 10], }); diff --git a/ui/src/app/edge/history/peakshaving/timeslot/chart.component.ts b/ui/src/app/edge/history/peakshaving/timeslot/chart.component.ts index 4a62f084032..f54aad37448 100644 --- a/ui/src/app/edge/history/peakshaving/timeslot/chart.component.ts +++ b/ui/src/app/edge/history/peakshaving/timeslot/chart.component.ts @@ -159,7 +159,7 @@ export class TimeslotPeakshavingChartComponent extends AbstractHistoryChart impl } }); datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargeData, borderDash: [10, 10], }); @@ -180,7 +180,7 @@ export class TimeslotPeakshavingChartComponent extends AbstractHistoryChart impl } }); datasets.push({ - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: dischargeData, borderDash: [10, 10], }); diff --git a/ui/src/app/edge/history/shared.ts b/ui/src/app/edge/history/shared.ts index ac05ac3b66f..8f997b3ceb7 100644 --- a/ui/src/app/edge/history/shared.ts +++ b/ui/src/app/edge/history/shared.ts @@ -1,7 +1,10 @@ // @ts-strict-ignore import * as Chart from "chart.js"; +/* eslint-disable import/no-duplicates */ +// cf. https://github.com/import-js/eslint-plugin-import/issues/1479 import { differenceInDays, differenceInMinutes, startOfDay } from "date-fns"; import { de } from "date-fns/locale"; +/* eslint-enable import/no-duplicates */ import { QueryHistoricTimeseriesDataResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesDataResponse"; import { ChannelAddress, Service } from "src/app/shared/shared"; import { DateUtils } from "src/app/shared/utils/date/dateutils"; diff --git a/ui/src/app/edge/history/storage/chargerchart.component.ts b/ui/src/app/edge/history/storage/chargerchart.component.ts index 44395de3842..d13d435b02f 100644 --- a/ui/src/app/edge/history/storage/chargerchart.component.ts +++ b/ui/src/app/edge/history/storage/chargerchart.component.ts @@ -70,7 +70,7 @@ export class StorageChargerChartComponent extends AbstractHistoryChart implement }); if (address.channelId == "ActualPower") { datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargerData, hidden: false, }); diff --git a/ui/src/app/edge/history/storage/singlechart.component.ts b/ui/src/app/edge/history/storage/singlechart.component.ts index 4d987a4b2ee..7bd11ca1a41 100644 --- a/ui/src/app/edge/history/storage/singlechart.component.ts +++ b/ui/src/app/edge/history/storage/singlechart.component.ts @@ -216,15 +216,15 @@ export class StorageSingleChartComponent extends AbstractHistoryChart implements // 0.005 to prevent showing Charge or Discharge if value is e.g. 0.00232138 if (value < -0.005) { if (label.includes(translate.instant("General.phase"))) { - label += " " + translate.instant("General.chargePower"); + label += " " + translate.instant("General.CHARGE"); } else { - label = translate.instant("General.chargePower"); + label = translate.instant("General.CHARGE"); } } else if (value > 0.005) { if (label.includes(translate.instant("General.phase"))) { - label += " " + translate.instant("General.dischargePower"); + label += " " + translate.instant("General.DISCHARGE"); } else { - label = translate.instant("General.dischargePower"); + label = translate.instant("General.DISCHARGE"); } } return label + ": " + formatNumber(value, "de", "1.0-2") + " kW"; diff --git a/ui/src/app/edge/history/storage/totalchart.component.ts b/ui/src/app/edge/history/storage/totalchart.component.ts index 13375ae97db..fb73aec189d 100644 --- a/ui/src/app/edge/history/storage/totalchart.component.ts +++ b/ui/src/app/edge/history/storage/totalchart.component.ts @@ -1,4 +1,5 @@ // @ts-strict-ignore +import { formatNumber } from "@angular/common"; import { Component, Input, OnChanges, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; @@ -7,7 +8,6 @@ import { DefaultTypes } from "src/app/shared/service/defaulttypes"; import { ChartAxis, Utils, YAxisType } from "src/app/shared/service/utils"; import { ChannelAddress, Edge, EdgeConfig, Service } from "src/app/shared/shared"; -import { formatNumber } from "@angular/common"; import { AbstractHistoryChart } from "../abstracthistorychart"; @Component({ @@ -252,9 +252,9 @@ export class StorageTotalChartComponent extends AbstractHistoryChart implements const value = tooltipItem.dataset.data[tooltipItem.dataIndex]; // 0.005 to prevent showing Charge or Discharge if value is e.g. 0.00232138 if (value < -0.005) { - label += " " + translate.instant("General.chargePower"); + label += " " + translate.instant("General.CHARGE"); } else if (value > 0.005) { - label += " " + translate.instant("General.dischargePower"); + label += " " + translate.instant("General.DISCHARGE"); } return label + ": " + formatNumber(value, "de", "1.0-2") + " kW"; }; diff --git a/ui/src/app/edge/history/storage/widget.component.html b/ui/src/app/edge/history/storage/widget.component.html index 79f3aaac4ce..6fc91033a0e 100644 --- a/ui/src/app/edge/history/storage/widget.component.html +++ b/ui/src/app/edge/history/storage/widget.component.html @@ -7,13 +7,13 @@ - + - + diff --git a/ui/src/app/edge/live/Controller/Channelthreshold/Channelthreshold.html b/ui/src/app/edge/live/Controller/Channelthreshold/Channelthreshold.html index 8cb74e66409..21cf476ec98 100644 --- a/ui/src/app/edge/live/Controller/Channelthreshold/Channelthreshold.html +++ b/ui/src/app/edge/live/Controller/Channelthreshold/Channelthreshold.html @@ -1,6 +1,6 @@ - + diff --git a/ui/src/app/edge/live/Controller/ChpSoc/ChpSoc.html b/ui/src/app/edge/live/Controller/ChpSoc/ChpSoc.html index 8dc374e330e..4e557816fc5 100644 --- a/ui/src/app/edge/live/Controller/ChpSoc/ChpSoc.html +++ b/ui/src/app/edge/live/Controller/ChpSoc/ChpSoc.html @@ -5,7 +5,7 @@ - + diff --git a/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/powerSocChart.ts b/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/powerSocChart.ts index 61e457c6d86..ed22926be64 100644 --- a/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/powerSocChart.ts +++ b/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/powerSocChart.ts @@ -146,7 +146,7 @@ export class SchedulePowerAndSocChartComponent extends AbstractHistoryChart impl datasets.push({ type: "line", - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: essChargeArray.map(v => Utils.divideSafely(v, 1000)), // [W] to [kW] hidden: true, order: 1, @@ -159,7 +159,7 @@ export class SchedulePowerAndSocChartComponent extends AbstractHistoryChart impl datasets.push({ type: "line", - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: essDischargeArray.map(v => Utils.divideSafely(v, 1000)), // [W] to [kW] hidden: true, order: 1, diff --git a/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/statePriceChart.ts b/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/statePriceChart.ts index 3e690c5828f..6061d016c14 100644 --- a/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/statePriceChart.ts +++ b/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/statePriceChart.ts @@ -4,13 +4,12 @@ import { ActivatedRoute } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import * as Chart from "chart.js"; import { AbstractHistoryChart } from "src/app/edge/history/abstracthistorychart"; +import { calculateResolution } from "src/app/edge/history/shared"; import { AbstractHistoryChart as NewAbstractHistoryChart } from "src/app/shared/components/chart/abstracthistorychart"; +import { ChartConstants } from "src/app/shared/components/chart/chart.constants"; import { ComponentJsonApiRequest } from "src/app/shared/jsonrpc/request/componentJsonApiRequest"; import { ChartAxis, HistoryUtils, TimeOfUseTariffUtils, YAxisType } from "src/app/shared/service/utils"; import { ChannelAddress, Currency, Edge, EdgeConfig, Service, Websocket } from "src/app/shared/shared"; - -import { calculateResolution } from "src/app/edge/history/shared"; -import { ChartConstants } from "src/app/shared/components/chart/chart.constants"; import { ColorUtils } from "src/app/shared/utils/color/color.utils"; import { GetScheduleRequest } from "../../../../../../shared/jsonrpc/request/getScheduleRequest"; import { GetScheduleResponse } from "../../../../../../shared/jsonrpc/response/getScheduleResponse"; @@ -108,11 +107,11 @@ export class ScheduleStateAndPriceChartComponent extends AbstractHistoryChart im private applyControllerSpecificOptions() { const locale = this.service.translate.currentLang; - const rightYaxisSoc: HistoryUtils.yAxes = { position: "right", unit: YAxisType.PERCENTAGE, yAxisId: ChartAxis.RIGHT }; - this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, rightYaxisSoc, this.translate, "line", locale, ChartConstants.EMPTY_DATASETS); + const rightYaxisSoc: HistoryUtils.yAxes = { position: "right", unit: YAxisType.PERCENTAGE, yAxisId: ChartAxis.RIGHT, displayGrid: true }; + this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, rightYaxisSoc, this.translate, "line", locale, this.datasets, true); const rightYAxisPower: HistoryUtils.yAxes = { position: "right", unit: YAxisType.POWER, yAxisId: ChartAxis.RIGHT_2 }; - this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, rightYAxisPower, this.translate, "line", locale, ChartConstants.EMPTY_DATASETS); + this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, rightYAxisPower, this.translate, "line", locale, this.datasets, true); this.options.scales.x["time"].unit = calculateResolution(this.service, this.service.historyPeriod.value.from, this.service.historyPeriod.value.to).timeFormat; this.options.scales.x["ticks"] = { source: "auto", autoSkip: false }; @@ -166,13 +165,19 @@ export class ScheduleStateAndPriceChartComponent extends AbstractHistoryChart im return el; }); + const leftYAxis: HistoryUtils.yAxes = { position: "left", unit: this.unit, yAxisId: ChartAxis.LEFT, customTitle: this.currencyLabel }; + [rightYaxisSoc, rightYAxisPower].forEach((element) => { + this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, element, this.translate, "line", locale, this.datasets, true); + }); - this.options.scales[ChartAxis.LEFT]["title"].text = this.currencyLabel; + this.options.scales[ChartAxis.LEFT] = { + ...this.options.scales[ChartAxis.LEFT], + ...ChartConstants.DEFAULT_Y_SCALE_OPTIONS(leftYAxis, this.translate, "line", this.datasets.filter(el => el["yAxisID"] === ChartAxis.LEFT), true), + }; this.options.scales[ChartAxis.RIGHT].grid.display = false; this.options.scales[ChartAxis.RIGHT_2].suggestedMin = 0; this.options.scales[ChartAxis.RIGHT_2].suggestedMax = 1; this.options.scales[ChartAxis.RIGHT_2].grid.display = false; this.options["animation"] = false; } - } diff --git a/ui/src/app/edge/live/Controller/Evcs/modal/modal.html b/ui/src/app/edge/live/Controller/Evcs/modal/modal.html index da7866b9130..0820cf1130a 100644 --- a/ui/src/app/edge/live/Controller/Evcs/modal/modal.html +++ b/ui/src/app/edge/live/Controller/Evcs/modal/modal.html @@ -32,6 +32,18 @@ [name]="'Edge.Index.Widgets.EVCS.energySinceBeginning' | translate" [value]="energySession"> + + + + + + + + + + +
    General.chargePowerGeneral.CHARGE {{ data["_sum/EssDcChargeEnergy"] | unitvalue:'kWh' }}
    General.dischargePowerGeneral.DISCHARGE {{ data["_sum/EssDcDischargeEnergy"] | unitvalue:'kWh' }}
    diff --git a/ui/src/app/edge/live/Controller/Evcs/modal/modal.ts b/ui/src/app/edge/live/Controller/Evcs/modal/modal.ts index 0e92bbc6eda..0b2e5053bc9 100644 --- a/ui/src/app/edge/live/Controller/Evcs/modal/modal.ts +++ b/ui/src/app/edge/live/Controller/Evcs/modal/modal.ts @@ -37,6 +37,7 @@ export class ModalComponent extends AbstractModal { protected isChargingEnabled: boolean = false; protected sessionLimit: number; protected helpKey: string; + protected awaitingHysteresis: boolean; constructor( @Inject(Websocket) protected override websocket: Websocket, @@ -116,11 +117,13 @@ export class ModalComponent extends AbstractModal { new ChannelAddress(this.controller?.id, "_PropertyChargeMode"), new ChannelAddress(this.controller?.id, "_PropertyEnabledCharging"), new ChannelAddress(this.controller?.id, "_PropertyDefaultChargeMinPower"), + new ChannelAddress(this.controller?.id, "AwaitingHysteresis"), ]; } protected override onCurrentData(currentData: CurrentData) { this.isConnectionSuccessful = currentData.allComponents[this.component.id + "/State"] !== 3 ? true : false; + this.awaitingHysteresis = currentData.allComponents[this.controller?.id + "/AwaitingHysteresis"]; // Do not change values after touching formControls if (this.formGroup?.pristine) { this.status = this.getState(this.controller ? currentData.allComponents[this.controller.id + "/_PropertyEnabledCharging"] === 1 : null, currentData.allComponents[this.component.id + "/Status"], currentData.allComponents[this.component.id + "/Plug"]); diff --git a/ui/src/app/edge/live/Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold.html b/ui/src/app/edge/live/Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold.html index 4d5daaa2a78..1154cfc7ba1 100644 --- a/ui/src/app/edge/live/Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold.html +++ b/ui/src/app/edge/live/Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold.html @@ -1,5 +1,5 @@ + [icon]="icon"> diff --git a/ui/src/app/edge/live/Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold.ts b/ui/src/app/edge/live/Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold.ts index bbed2715a18..b2ac0822479 100644 --- a/ui/src/app/edge/live/Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold.ts +++ b/ui/src/app/edge/live/Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold.ts @@ -19,7 +19,7 @@ export class Controller_Io_ChannelSingleThresholdComponent extends AbstractFlatW public icon: Icon = { name: "", color: "", - size: "", + size: "large", }; public dependendOn: string; public dependendOnValue: any; diff --git a/ui/src/app/edge/live/common/consumption/flat/flat.ts b/ui/src/app/edge/live/common/consumption/flat/flat.ts index 85b0069083f..58c13f96316 100644 --- a/ui/src/app/edge/live/common/consumption/flat/flat.ts +++ b/ui/src/app/edge/live/common/consumption/flat/flat.ts @@ -50,8 +50,11 @@ export class FlatComponent extends AbstractFlatWidget { // Get EVCSs this.evcss = this.config.getComponentsImplementingNature("io.openems.edge.evcs.api.Evcs") - .filter(component => !(component.factoryId == "Evcs.Cluster.SelfConsumption") && - !(component.factoryId == "Evcs.Cluster.PeakShaving") && !component.isEnabled == false); + .filter(component => + !(component.factoryId == "Evcs.Cluster.SelfConsumption") && + !(component.factoryId == "Evcs.Cluster.PeakShaving") && + !(this.config.factories[component.factoryId].natureIds.includes("io.openems.edge.meter.api.ElectricityMeter")) && + !component.isEnabled == false); for (const component of this.evcss) { channelAddresses.push( diff --git a/ui/src/app/edge/live/common/consumption/modal/modal.ts b/ui/src/app/edge/live/common/consumption/modal/modal.ts index ccdcd9af819..a228215317f 100644 --- a/ui/src/app/edge/live/common/consumption/modal/modal.ts +++ b/ui/src/app/edge/live/common/consumption/modal/modal.ts @@ -16,8 +16,11 @@ export class ModalComponent extends AbstractFormlyComponent { public static generateView(config: EdgeConfig, translate: TranslateService): OeFormlyView { const evcss: EdgeConfig.Component[] | null = config.getComponentsImplementingNature("io.openems.edge.evcs.api.Evcs") - .filter(component => !(component.factoryId == "Evcs.Cluster.SelfConsumption") && - !(component.factoryId == "Evcs.Cluster.PeakShaving") && !component.isEnabled == false); + .filter(component => + !(component.factoryId == "Evcs.Cluster.SelfConsumption") && + !(component.factoryId == "Evcs.Cluster.PeakShaving") && + !(config.factories[component.factoryId].natureIds.includes("io.openems.edge.meter.api.ElectricityMeter")) && + !component.isEnabled == false); const consumptionMeters: EdgeConfig.Component[] | null = config.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter") .filter(component => component.isEnabled && config.isTypeConsumptionMetered(component)); diff --git a/ui/src/app/edge/live/common/storage/modal/modal.component.html b/ui/src/app/edge/live/common/storage/modal/modal.component.html index 4e83cd402e7..1a69b3ebe9b 100644 --- a/ui/src/app/edge/live/common/storage/modal/modal.component.html +++ b/ui/src/app/edge/live/common/storage/modal/modal.component.html @@ -30,12 +30,12 @@ - + - + @@ -52,7 +52,7 @@ - + - + @@ -340,7 +340,7 @@ - + - + - + - + diff --git a/ui/src/app/edge/live/common/storage/storage.component.html b/ui/src/app/edge/live/common/storage/storage.component.html index d7ffcb21081..47d39d8842e 100644 --- a/ui/src/app/edge/live/common/storage/storage.component.html +++ b/ui/src/app/edge/live/common/storage/storage.component.html @@ -15,10 +15,9 @@ - - + + @@ -31,20 +30,20 @@ - + - - - diff --git a/ui/src/app/edge/live/common/storage/storage.component.ts b/ui/src/app/edge/live/common/storage/storage.component.ts index ca1846f220f..8114c49b3b3 100644 --- a/ui/src/app/edge/live/common/storage/storage.component.ts +++ b/ui/src/app/edge/live/common/storage/storage.component.ts @@ -2,10 +2,9 @@ import { formatNumber } from "@angular/common"; import { Component } from "@angular/core"; import { AbstractFlatWidget } from "src/app/shared/components/flat/abstract-flat-widget"; -import { CurrentData } from "src/app/shared/shared"; +import { CurrentData , ChannelAddress, EdgeConfig, Utils } from "src/app/shared/shared"; import { DateUtils } from "src/app/shared/utils/date/dateutils"; -import { ChannelAddress, EdgeConfig, Utils } from "../../../../shared/shared"; import { StorageModalComponent } from "./modal/modal.component"; @Component({ diff --git a/ui/src/app/edge/live/energymonitor/chart/chart.component.html b/ui/src/app/edge/live/energymonitor/chart/chart.component.html index c2eddea61c8..d27b5bac185 100644 --- a/ui/src/app/edge/live/energymonitor/chart/chart.component.html +++ b/ui/src/app/edge/live/energymonitor/chart/chart.component.html @@ -1,7 +1,7 @@
    - + diff --git a/ui/src/app/edge/live/live.component.html b/ui/src/app/edge/live/live.component.html index 88a68dd2777..9d4cf55d172 100644 --- a/ui/src/app/edge/live/live.component.html +++ b/ui/src/app/edge/live/live.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/live/live.module.ts b/ui/src/app/edge/live/live.module.ts index 944e2240b03..ffec734d90c 100644 --- a/ui/src/app/edge/live/live.module.ts +++ b/ui/src/app/edge/live/live.module.ts @@ -1,15 +1,23 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { EdgeOfflineModule } from "src/app/shared/components/edge/offline/offline.module"; import { SharedModule } from "./../../shared/shared.module"; +import { Common_Autarchy } from "./common/autarchy/Common_Autarchy"; +import { Common_Consumption } from "./common/consumption/Common_Consumption"; +import { Common_Grid } from "./common/grid/Common_Grid"; +import { Common_Production } from "./common/production/Common_Production"; +import { Common_Selfconsumption } from "./common/selfconsumption/Common_Selfconsumption"; +import { StorageModalComponent } from "./common/storage/modal/modal.component"; +import { StorageComponent } from "./common/storage/storage.component"; import { Controller_ChannelthresholdComponent } from "./Controller/Channelthreshold/Channelthreshold"; import { Controller_ChpSocComponent } from "./Controller/ChpSoc/ChpSoc"; import { Controller_ChpSocModalComponent } from "./Controller/ChpSoc/modal/modal.component"; import { Controller_Ess_FixActivePower } from "./Controller/Ess/FixActivePower/Ess_FixActivePower"; import { Controller_Ess_GridOptimizedCharge } from "./Controller/Ess/GridOptimizedCharge/Ess_GridOptimizedCharge"; import { Controller_Ess_TimeOfUseTariff } from "./Controller/Ess/TimeOfUseTariff/Ess_TimeOfUseTariff"; -import { Controller_Evcs } from "./Controller/Evcs/Evcs"; import { AdministrationComponent } from "./Controller/Evcs/administration/administration.component"; +import { Controller_Evcs } from "./Controller/Evcs/Evcs"; import { Controller_Io_ChannelSingleThresholdComponent } from "./Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold"; import { Controller_Io_ChannelSingleThresholdModalComponent } from "./Controller/Io/ChannelSingleThreshold/modal/modal.component"; import { Controller_Io_FixDigitalOutputComponent } from "./Controller/Io/FixDigitalOutput/Io_FixDigitalOutput"; @@ -20,48 +28,39 @@ import { Controller_Io_HeatpumpModalComponent } from "./Controller/Io/Heatpump/m import { Controller_Api_ModbusTcp } from "./Controller/ModbusTcpApi/modbusTcpApi.module"; import { Controller_Asymmetric_PeakShavingComponent } from "./Controller/PeakShaving/Asymmetric/Asymmetric"; import { Controller_Asymmetric_PeakShavingModalComponent } from "./Controller/PeakShaving/Asymmetric/modal/modal.component"; -import { Controller_Symmetric_PeakShavingComponent } from "./Controller/PeakShaving/Symmetric/Symmetric"; import { Controller_Symmetric_PeakShavingModalComponent } from "./Controller/PeakShaving/Symmetric/modal/modal.component"; -import { Controller_Symmetric_TimeSlot_PeakShavingComponent } from "./Controller/PeakShaving/Symmetric_TimeSlot/Symmetric_TimeSlot"; +import { Controller_Symmetric_PeakShavingComponent } from "./Controller/PeakShaving/Symmetric/Symmetric"; import { Controller_Symmetric_TimeSlot_PeakShavingModalComponent } from "./Controller/PeakShaving/Symmetric_TimeSlot/modal/modal.component"; -import { Io_Api_DigitalInputComponent } from "./Io/Api_DigitalInput/Io_Api_DigitalInput"; -import { Io_Api_DigitalInput_ModalComponent } from "./Io/Api_DigitalInput/modal/modal.component"; -import { Evcs_Api_ClusterComponent } from "./Multiple/Evcs_Api_Cluster/Evcs_Api_Cluster"; -import { EvcsChartComponent } from "./Multiple/Evcs_Api_Cluster/modal/evcs-chart/evcs.chart"; -import { Evcs_Api_ClusterModalComponent } from "./Multiple/Evcs_Api_Cluster/modal/evcsCluster-modal.page"; -import { Common_Autarchy } from "./common/autarchy/Common_Autarchy"; -import { Common_Consumption } from "./common/consumption/Common_Consumption"; -import { Common_Grid } from "./common/grid/Common_Grid"; -import { Common_Production } from "./common/production/Common_Production"; -import { Common_Selfconsumption } from "./common/selfconsumption/Common_Selfconsumption"; -import { StorageModalComponent } from "./common/storage/modal/modal.component"; -import { StorageComponent } from "./common/storage/storage.component"; +import { Controller_Symmetric_TimeSlot_PeakShavingComponent } from "./Controller/PeakShaving/Symmetric_TimeSlot/Symmetric_TimeSlot"; import { DelayedSellToGridComponent } from "./delayedselltogrid/delayedselltogrid.component"; import { DelayedSellToGridModalComponent } from "./delayedselltogrid/modal/modal.component"; import { EnergymonitorModule } from "./energymonitor/energymonitor.module"; import { InfoComponent } from "./info/info.component"; +import { Io_Api_DigitalInputComponent } from "./Io/Api_DigitalInput/Io_Api_DigitalInput"; +import { Io_Api_DigitalInput_ModalComponent } from "./Io/Api_DigitalInput/modal/modal.component"; import { LiveComponent } from "./live.component"; -import { OfflineComponent } from "./offline/offline.component"; +import { Evcs_Api_ClusterComponent } from "./Multiple/Evcs_Api_Cluster/Evcs_Api_Cluster"; +import { EvcsChartComponent } from "./Multiple/Evcs_Api_Cluster/modal/evcs-chart/evcs.chart"; +import { Evcs_Api_ClusterModalComponent } from "./Multiple/Evcs_Api_Cluster/modal/evcsCluster-modal.page"; @NgModule({ imports: [ BrowserAnimationsModule, BrowserModule, - // Common Common_Autarchy, - Common_Production, - Common_Selfconsumption, Common_Consumption, Common_Grid, - // Controller + Common_Production, + Common_Selfconsumption, + Controller_Api_ModbusTcp, Controller_Ess_FixActivePower, Controller_Ess_GridOptimizedCharge, + Controller_Ess_TimeOfUseTariff, + Controller_Evcs, Controller_Io_HeatingElement, - Controller_Api_ModbusTcp, + EdgeOfflineModule, EnergymonitorModule, SharedModule, - Controller_Evcs, - Controller_Ess_TimeOfUseTariff, ], declarations: [ AdministrationComponent, @@ -90,7 +89,6 @@ import { OfflineComponent } from "./offline/offline.component"; Io_Api_DigitalInput_ModalComponent, Io_Api_DigitalInputComponent, LiveComponent, - OfflineComponent, StorageComponent, StorageModalComponent, ], diff --git a/ui/src/app/edge/settings/alerting/alerting.component.html b/ui/src/app/edge/settings/alerting/alerting.component.html index 0177269976c..f674bd30749 100644 --- a/ui/src/app/edge/settings/alerting/alerting.component.html +++ b/ui/src/app/edge/settings/alerting/alerting.component.html @@ -6,7 +6,7 @@ vertical-align: middle; } -
    + diff --git a/ui/src/app/edge/settings/app/index.component.html b/ui/src/app/edge/settings/app/index.component.html index 48926c47bdf..b7225ae5673 100644 --- a/ui/src/app/edge/settings/app/index.component.html +++ b/ui/src/app/edge/settings/app/index.component.html @@ -3,7 +3,6 @@ - {{ app.shortName ?? app.name }} @@ -61,7 +60,7 @@

    -
    + diff --git a/ui/src/app/edge/settings/app/install.component.html b/ui/src/app/edge/settings/app/install.component.html index 2418d652e3a..9b805f1fd96 100644 --- a/ui/src/app/edge/settings/app/install.component.html +++ b/ui/src/app/edge/settings/app/install.component.html @@ -1,4 +1,3 @@ -
    @@ -8,7 +7,6 @@
    - {{ appName }} diff --git a/ui/src/app/edge/settings/app/single.component.html b/ui/src/app/edge/settings/app/single.component.html index 465413bd056..3896e3aa39c 100644 --- a/ui/src/app/edge/settings/app/single.component.html +++ b/ui/src/app/edge/settings/app/single.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/app/update.component.html b/ui/src/app/edge/settings/app/update.component.html index 039d79b1826..dff8a08141c 100644 --- a/ui/src/app/edge/settings/app/update.component.html +++ b/ui/src/app/edge/settings/app/update.component.html @@ -1,4 +1,3 @@ -
    @@ -6,7 +5,6 @@ - {{ appName }} diff --git a/ui/src/app/edge/settings/channels/channels.component.html b/ui/src/app/edge/settings/channels/channels.component.html index ecef95c38f1..c0931184525 100644 --- a/ui/src/app/edge/settings/channels/channels.component.html +++ b/ui/src/app/edge/settings/channels/channels.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/channels/channels.component.ts b/ui/src/app/edge/settings/channels/channels.component.ts index 36ea1f7911c..e8f2411c1f0 100644 --- a/ui/src/app/edge/settings/channels/channels.component.ts +++ b/ui/src/app/edge/settings/channels/channels.component.ts @@ -3,12 +3,12 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import { PersistencePriority } from "src/app/shared/components/edge/edgeconfig"; -import { SetChannelValueRequest } from "src/app/shared/jsonrpc/request/setChannelValueRequest"; -import { environment } from "src/environments"; import { ComponentJsonApiRequest } from "src/app/shared/jsonrpc/request/componentJsonApiRequest"; import { GetChannelsOfComponentRequest } from "src/app/shared/jsonrpc/request/getChannelsOfComponentRequest"; +import { SetChannelValueRequest } from "src/app/shared/jsonrpc/request/setChannelValueRequest"; import { Channel, GetChannelsOfComponentResponse } from "src/app/shared/jsonrpc/response/getChannelsOfComponentResponse"; +import { environment } from "src/environments"; import { ChannelAddress, Edge, EdgeConfig, EdgePermission, Service, Websocket } from "../../../shared/shared"; @Component({ diff --git a/ui/src/app/edge/settings/component/install/index.component.html b/ui/src/app/edge/settings/component/install/index.component.html index c918b508485..bf72bc8faba 100644 --- a/ui/src/app/edge/settings/component/install/index.component.html +++ b/ui/src/app/edge/settings/component/install/index.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/component/install/install.component.html b/ui/src/app/edge/settings/component/install/install.component.html index ae3916b1def..39bd2dfbb1a 100644 --- a/ui/src/app/edge/settings/component/install/install.component.html +++ b/ui/src/app/edge/settings/component/install/install.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/component/update/index.component.html b/ui/src/app/edge/settings/component/update/index.component.html index 30373db23e2..2cd3b5e7223 100644 --- a/ui/src/app/edge/settings/component/update/index.component.html +++ b/ui/src/app/edge/settings/component/update/index.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/component/update/update.component.html b/ui/src/app/edge/settings/component/update/update.component.html index aeaee6f0114..793ad16cfed 100644 --- a/ui/src/app/edge/settings/component/update/update.component.html +++ b/ui/src/app/edge/settings/component/update/update.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/jsonrpctest/jsonrpctest.html b/ui/src/app/edge/settings/jsonrpctest/jsonrpctest.html index 0468ec0c4d5..9359068039c 100644 --- a/ui/src/app/edge/settings/jsonrpctest/jsonrpctest.html +++ b/ui/src/app/edge/settings/jsonrpctest/jsonrpctest.html @@ -31,7 +31,6 @@ -
    diff --git a/ui/src/app/edge/settings/network/network.component.html b/ui/src/app/edge/settings/network/network.component.html index aaae39ad4ae..1526c04e99c 100644 --- a/ui/src/app/edge/settings/network/network.component.html +++ b/ui/src/app/edge/settings/network/network.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/powerassistant/powerassistant.html b/ui/src/app/edge/settings/powerassistant/powerassistant.html index 8fdb3666116..e428d16d831 100644 --- a/ui/src/app/edge/settings/powerassistant/powerassistant.html +++ b/ui/src/app/edge/settings/powerassistant/powerassistant.html @@ -7,7 +7,7 @@ -
    + diff --git a/ui/src/app/edge/settings/profile/aliasupdate.component.html b/ui/src/app/edge/settings/profile/aliasupdate.component.html index 1e63df550f6..47efa868cca 100644 --- a/ui/src/app/edge/settings/profile/aliasupdate.component.html +++ b/ui/src/app/edge/settings/profile/aliasupdate.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/profile/profile.component.html b/ui/src/app/edge/settings/profile/profile.component.html index a5adddc50c3..2e9b3884104 100644 --- a/ui/src/app/edge/settings/profile/profile.component.html +++ b/ui/src/app/edge/settings/profile/profile.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/settings.component.html b/ui/src/app/edge/settings/settings.component.html index abfdf9a2a04..4c22db2268f 100644 --- a/ui/src/app/edge/settings/settings.component.html +++ b/ui/src/app/edge/settings/settings.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/settings.module.ts b/ui/src/app/edge/settings/settings.module.ts index 3e12f05e109..0f252dc9277 100644 --- a/ui/src/app/edge/settings/settings.module.ts +++ b/ui/src/app/edge/settings/settings.module.ts @@ -1,6 +1,5 @@ import { NgModule } from "@angular/core"; import { ChangelogModule } from "src/app/changelog/changelog.module"; - import { SharedModule } from "./../../shared/shared.module"; import { AlertingComponent } from "./alerting/alerting.component"; import { AppModule } from "./app/app.module"; diff --git a/ui/src/app/edge/settings/system/system.component.html b/ui/src/app/edge/settings/system/system.component.html index fb00b72fee6..154e66467b4 100644 --- a/ui/src/app/edge/settings/system/system.component.html +++ b/ui/src/app/edge/settings/system/system.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/system/system.component.ts b/ui/src/app/edge/settings/system/system.component.ts index e6398f41b90..4040aeea1a7 100644 --- a/ui/src/app/edge/settings/system/system.component.ts +++ b/ui/src/app/edge/settings/system/system.component.ts @@ -1,6 +1,5 @@ // @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { Component, effect } from "@angular/core"; import { environment } from "src/environments"; import { Edge, Service, UserPermission, Utils } from "../../../shared/shared"; @@ -8,30 +7,26 @@ import { Edge, Service, UserPermission, Utils } from "../../../shared/shared"; selector: SystemComponent.SELECTOR, templateUrl: "./system.component.html", }) -export class SystemComponent implements OnInit { +export class SystemComponent { private static readonly SELECTOR = "system"; - public readonly environment = environment; - public readonly spinnerId: string = SystemComponent.SELECTOR; - public showLog: boolean = false; - public readonly ESTIMATED_REBOOT_TIME = 600; // Seconds till the openems service is restarted after update - - public edge: Edge; - public restartTime: number = this.ESTIMATED_REBOOT_TIME; - + protected readonly environment = environment; + protected readonly spinnerId: string = SystemComponent.SELECTOR; + protected showLog: boolean = false; + protected readonly ESTIMATED_REBOOT_TIME = 600; // Seconds till the openems service is restarted after update + protected edge: Edge; + protected restartTime: number = this.ESTIMATED_REBOOT_TIME; protected canSeeSystemRestart: boolean = false; constructor( - private route: ActivatedRoute, protected utils: Utils, private service: Service, - ) { } - - ngOnInit() { - this.service.getCurrentEdge().then(edge => { - this.edge = edge; - this.canSeeSystemRestart = UserPermission.isAllowedToSeeSystemRestart(this.service.currentUser, edge); + ) { + effect(async () => { + const user = this.service.currentUser(); + this.edge = await this.service.getCurrentEdge(); + this.canSeeSystemRestart = UserPermission.isAllowedToSeeSystemRestart(user, this.edge); }); } } diff --git a/ui/src/app/edge/settings/systemexecute/systemexecute.component.html b/ui/src/app/edge/settings/systemexecute/systemexecute.component.html index c22504d89c5..82cf2b8c0f7 100644 --- a/ui/src/app/edge/settings/systemexecute/systemexecute.component.html +++ b/ui/src/app/edge/settings/systemexecute/systemexecute.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/edge/settings/systemlog/systemlog.component.html b/ui/src/app/edge/settings/systemlog/systemlog.component.html index 37919bc7547..9acbac2f1da 100644 --- a/ui/src/app/edge/settings/systemlog/systemlog.component.html +++ b/ui/src/app/edge/settings/systemlog/systemlog.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/index/login.component.ts b/ui/src/app/index/login.component.ts index 4fa20f9aeff..63b7077c7e7 100644 --- a/ui/src/app/index/login.component.ts +++ b/ui/src/app/index/login.component.ts @@ -2,10 +2,11 @@ import { AfterContentChecked, ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; import { FormGroup } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; +import { Capacitor } from "@capacitor/core"; +import { ViewWillEnter } from "@ionic/angular"; import { Subject } from "rxjs"; import { environment } from "src/environments"; -import { Capacitor } from "@capacitor/core"; import { AppService } from "../app.service"; import { AuthenticateWithPasswordRequest } from "../shared/jsonrpc/request/authenticateWithPasswordRequest"; import { States } from "../shared/ngrx-store/states"; @@ -15,7 +16,7 @@ import { Edge, Service, Utils, Websocket } from "../shared/shared"; selector: "login", templateUrl: "./login.component.html", }) -export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { +export class LoginComponent implements ViewWillEnter, AfterContentChecked, OnDestroy, OnInit { public environment = environment; public form: FormGroup; protected formIsDisabled: boolean = false; @@ -35,16 +36,16 @@ export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { ) { } /** - * Trims credentials - * - * @param password the password - * @param username the username - * @returns trimmed credentials - */ - public static trimCredentials(password: string, username?: string): { password: string, username?: string } { + * Preprocesses the credentials + * + * @param password the password + * @param username the username + * @returns trimmed credentials + */ + public static preprocessCredentials(password: string, username?: string): { password: string, username?: string } { return { password: password?.trim(), - ...(username && { username: username?.trim() }), + ...(username && { username: username?.trim().toLowerCase() }), }; } @@ -53,18 +54,16 @@ export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { } ngOnInit() { - - // TODO add websocket status observable const interval = setInterval(() => { - if (this.websocket.status === "online") { + if (this.websocket.status === "online" && !this.router.url.split("/").includes("live")) { this.router.navigate(["/overview"]); clearInterval(interval); } }, 1000); } - async ionViewWillEnter() { + async ionViewWillEnter() { // Execute Login-Request if url path matches 'demo' if (this.route.snapshot.routeConfig.path == "demo") { @@ -95,7 +94,7 @@ export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { public doLogin(param: { username?: string, password: string }) { this.websocket.state.set(States.AUTHENTICATION_WITH_CREDENTIALS); - param = LoginComponent.trimCredentials(param.password, param.username); + param = LoginComponent.preprocessCredentials(param.password, param.username); // Prevent that user submits via keyevent 'enter' multiple times if (this.formIsDisabled) { @@ -107,7 +106,7 @@ export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { .finally(() => { // Unclean - this.ngOnInit(); + this.ionViewWillEnter(); this.formIsDisabled = false; }); } diff --git a/ui/src/app/index/login.spec.ts b/ui/src/app/index/login.spec.ts index dad874f51d4..9feea8aa9ce 100644 --- a/ui/src/app/index/login.spec.ts +++ b/ui/src/app/index/login.spec.ts @@ -12,26 +12,30 @@ describe("Login", () => { }).compileComponents(); }); - it("#trimCredentials should trim password and username", () => { + it("#preprocessCredentials should trim password and username and should lowerCase username", () => { { // Username and password - OpenEMS Backend - expect(LoginComponent.trimCredentials(password, username)).toEqual({ password: "password", username: "username" }); + expect(LoginComponent.preprocessCredentials(password, username)).toEqual({ password: "password", username: "username" }); } { // Only Password - OpenEMS Edge - expect(LoginComponent.trimCredentials(password)).toEqual({ password: "password" }); + expect(LoginComponent.preprocessCredentials(password)).toEqual({ password: "password" }); } { // Password is null - expect(LoginComponent.trimCredentials(null)).toEqual({ password: undefined }); + expect(LoginComponent.preprocessCredentials(null)).toEqual({ password: undefined }); } { // Username is null - expect(LoginComponent.trimCredentials(password, null)).toEqual({ password: "password" }); + expect(LoginComponent.preprocessCredentials(password, null)).toEqual({ password: "password" }); } { // Username and password are null - expect(LoginComponent.trimCredentials(null, null)).toEqual({ password: undefined }); + expect(LoginComponent.preprocessCredentials(null, null)).toEqual({ password: undefined }); + } + { + // Username in Upper case + expect(LoginComponent.preprocessCredentials(password, username.toUpperCase())).toEqual({ password: "password", username: "username" }); } }); }); diff --git a/ui/src/app/index/overview/overview.component.html b/ui/src/app/index/overview/overview.component.html index e1a0fa4e05d..e4c411d6833 100644 --- a/ui/src/app/index/overview/overview.component.html +++ b/ui/src/app/index/overview/overview.component.html @@ -1,4 +1,3 @@ -
    diff --git a/ui/src/app/index/overview/overview.component.ts b/ui/src/app/index/overview/overview.component.ts index f8a129eaa3a..8f885578816 100644 --- a/ui/src/app/index/overview/overview.component.ts +++ b/ui/src/app/index/overview/overview.component.ts @@ -1,8 +1,8 @@ // @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy } from "@angular/core"; import { FormGroup } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { InfiniteScrollCustomEvent } from "@ionic/angular"; +import { InfiniteScrollCustomEvent, ViewWillEnter } from "@ionic/angular"; import { TranslateService } from "@ngx-translate/core"; import { Subject } from "rxjs"; import { filter, take } from "rxjs/operators"; @@ -10,14 +10,13 @@ import { Pagination } from "src/app/shared/service/pagination"; import { Edge, Service, Utils, Websocket } from "src/app/shared/shared"; import { Role } from "src/app/shared/type/role"; import { environment } from "src/environments"; - import { ChosenFilter } from "../filter/filter.component"; @Component({ selector: "overview", templateUrl: "./overview.component.html", }) -export class OverViewComponent implements OnInit, OnDestroy { +export class OverViewComponent implements ViewWillEnter, OnDestroy { public environment = environment; /** True, if there is no access to any Edge. */ public noEdges: boolean = false; @@ -50,7 +49,7 @@ export class OverViewComponent implements OnInit, OnDestroy { public pagination: Pagination, ) { } - ngOnInit() { + ionViewWillEnter() { this.page = 0; this.filteredEdges = []; this.limitReached = false; @@ -139,7 +138,6 @@ export class OverViewComponent implements OnInit, OnDestroy { take(1), ) .subscribe(metadata => { - const edgeIds = Object.keys(metadata.edges); this.noEdges = edgeIds.length === 0; this.loggedInUserCanInstall = Role.isAtLeast(metadata.user.globalRole, "installer"); diff --git a/ui/src/app/shared/components/abstracthistorywidget.ts b/ui/src/app/shared/components/abstracthistorywidget.ts index dfc48dffa2a..76204d7e133 100644 --- a/ui/src/app/shared/components/abstracthistorywidget.ts +++ b/ui/src/app/shared/components/abstracthistorywidget.ts @@ -4,9 +4,9 @@ import { ActivatedRoute } from "@angular/router"; import { ModalController } from "@ionic/angular"; import { TranslateService } from "@ngx-translate/core"; import { Subject } from "rxjs"; +import { v4 as uuidv4 } from "uuid"; import { DefaultTypes } from "src/app/shared/service/defaulttypes"; import { ChannelAddress, CurrentData, Edge, EdgeConfig, Service, Websocket } from "src/app/shared/shared"; -import { v4 as uuidv4 } from "uuid"; // NOTE: Auto-refresh of widgets is currently disabled to reduce server load @Directive() diff --git a/ui/src/app/shared/components/chart/abstracthistorychart.ts b/ui/src/app/shared/components/chart/abstracthistorychart.ts index b30e69af4b8..5d4969e8cc8 100644 --- a/ui/src/app/shared/components/chart/abstracthistorychart.ts +++ b/ui/src/app/shared/components/chart/abstracthistorychart.ts @@ -4,13 +4,15 @@ import { ChangeDetectorRef, Directive, Input, OnDestroy, OnInit } from "@angular import { ActivatedRoute } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import * as Chart from "chart.js"; +import "chartjs-adapter-date-fns"; import annotationPlugin from "chartjs-plugin-annotation"; +import { v4 as uuidv4 } from "uuid"; + import { ChronoUnit, DEFAULT_NUMBER_CHART_OPTIONS, DEFAULT_TIME_CHART_OPTIONS, Resolution, calculateResolution, isLabelVisible, setLabelVisible } from "src/app/edge/history/shared"; import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse"; import { DefaultTypes } from "src/app/shared/service/defaulttypes"; -import { v4 as uuidv4 } from "uuid"; - import { JsonrpcResponseError } from "../../jsonrpc/base"; +import { JsonRpcUtils } from "../../jsonrpc/jsonrpcutils"; import { QueryHistoricTimeseriesDataRequest } from "../../jsonrpc/request/queryHistoricTimeseriesDataRequest"; import { QueryHistoricTimeseriesEnergyPerPeriodRequest } from "../../jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest"; import { QueryHistoricTimeseriesEnergyRequest } from "../../jsonrpc/request/queryHistoricTimeseriesEnergyRequest"; @@ -27,8 +29,6 @@ import { TimeUtils } from "../../utils/time/timeutils"; import { Converter } from "../shared/converter"; import { ChartConstants, XAxisType } from "./chart.constants"; -import "chartjs-adapter-date-fns"; - Chart.Chart.register(annotationPlugin); // NOTE: Auto-refresh of widgets is currently disabled to reduce server load @@ -166,15 +166,20 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { public static fillData(element: HistoryUtils.DisplayValue, label: string, chartObject: HistoryUtils.ChartData, chartType: "line" | "bar", data: number[] | null): { datasets: Chart.ChartDataset[], legendOptions: { label: string, strokeThroughHidingStyle: boolean, hideLabelInLegend: boolean; }[]; } { const legendOptions: { label: string, strokeThroughHidingStyle: boolean, hideLabelInLegend: boolean; }[] = []; const datasets: Chart.ChartDataset[] = []; + let normalizedData: (number | null)[] = data; + + if (chartObject.normalizeOutputData == true) { + normalizedData = JsonRpcUtils.normalizeQueryData(data); + } // Enable one dataset to be displayed in multiple stacks if (Array.isArray(element.stack)) { for (const stack of element.stack) { - datasets.push(AbstractHistoryChart.getDataSet(element, label, data, stack, chartObject, element.custom?.type ?? chartType)); + datasets.push(AbstractHistoryChart.getDataSet(element, label, normalizedData, stack, chartObject, element.custom?.type ?? chartType)); legendOptions.push(AbstractHistoryChart.getLegendOptions(label, element)); } } else { - datasets.push(AbstractHistoryChart.getDataSet(element, label, data, element.stack, chartObject, element.custom?.type ?? chartType)); + datasets.push(AbstractHistoryChart.getDataSet(element, label, normalizedData, element.stack, chartObject, element.custom?.type ?? chartType)); legendOptions.push(AbstractHistoryChart.getLegendOptions(label, element)); } @@ -310,7 +315,7 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { case YAxisType.TIME: return translate.instant("Edge.Index.Widgets.Channeltreshold.ACTIVE_TIME_OVER_PERIOD"); case YAxisType.PERCENTAGE: - return translate.instant("General.percentage"); + return "%"; case YAxisType.REACTIVE: return "var"; case YAxisType.ENERGY: @@ -320,9 +325,9 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { return "kW"; } case YAxisType.VOLTAGE: - return translate.instant("Edge.History.VOLTAGE"); + return "V"; case YAxisType.CURRENT: - return translate.instant("Edge.History.CURRENT"); + return "A"; case YAxisType.NONE: return ""; default: @@ -358,9 +363,8 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { let options: Chart.ChartOptions = Utils.deepCopy(Utils.deepCopy(AbstractHistoryChart.getDefaultOptions(chartOptionsType, service, labels))); const displayValues: HistoryUtils.DisplayValue[] = chartObject.output(channelData.data); - const showYAxisType: boolean = chartObject.yAxes.length > 1; chartObject.yAxes.forEach((element) => { - options = AbstractHistoryChart.getYAxisOptions(options, element, translate, chartType, locale, datasets, showYAxisType); + options = AbstractHistoryChart.getYAxisOptions(options, element, translate, chartType, locale, datasets, true); }); options.plugins.tooltip.callbacks.title = (tooltipItems: Chart.TooltipItem[]): string => { @@ -401,6 +405,7 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { }; }; + options.plugins.legend.labels.generateLabels = function (chart: Chart.Chart) { const chartLegendLabelItems: Chart.LegendItem[] = []; @@ -460,10 +465,19 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { return null; }; + options.plugins.tooltip.enabled = chartObject.tooltip.enabled ?? true; + // Remove duplicates from legend, if legendItem with two or more occurrences in legend, use one legendItem to trigger them both - options.plugins.legend.onClick = function (event: Chart.ChartEvent, legendItem: Chart.LegendItem, legend) { + options.plugins.legend.onClick = function (event: Chart.ChartEvent, legendItem: Chart.LegendItem, legend: Chart.LegendElement) { const chart: Chart.Chart = this.chart; + function rebuildScales(chart: Chart.Chart) { + let options = chart.options; + chartObject.yAxes.forEach((element) => { + options = AbstractHistoryChart.getYAxisOptions(options, element, translate, chartType, locale, _dataSets, true); + }); + } + const legendItems = chart.data.datasets.reduce((arr, ds, i) => { if (ds.label == legendItem.text) { arr.push({ label: ds.label, index: i }); @@ -479,14 +493,22 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { meta.hidden = meta.hidden === null ? !chart.data.datasets[item.index].hidden : null; }); - // We hid a dataset ... rerender the chart + /** needs to be set, cause property async set */ + const _dataSets: Chart.ChartDataset[] = datasets.map((v, k) => { + if (k === legendItem.datasetIndex) { + v.hidden = !v.hidden; + } + return v; + }); + + rebuildScales(chart); chart.update(); }; + options.scales.x.ticks["source"] = "auto"; options.scales.x.ticks.maxTicksLimit = 31; options.scales.x["bounds"] = "ticks"; - options; options = AbstractHistoryChart.getExternalPluginFeatures(displayValues, options, chartType); return options; @@ -505,9 +527,7 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { public static getYAxisOptions(options: Chart.ChartOptions, element: HistoryUtils.yAxes, translate: TranslateService, chartType: "line" | "bar", locale: string, datasets: Chart.ChartDataset[], showYAxisType?: boolean): Chart.ChartOptions { const baseConfig = ChartConstants.DEFAULT_Y_SCALE_OPTIONS(element, translate, chartType, datasets, showYAxisType); - switch (element.unit) { - case YAxisType.RELAY: options.scales[element.yAxisId] = { ...baseConfig, @@ -518,7 +538,7 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { ...baseConfig.ticks, stepSize: 1, // Two states are possible - callback: function (value, index, ticks) { + callback: function (value, index, ticks: Chart.Tick[]) { return Converter.ON_OFF(translate)(value); }, padding: 5, @@ -556,13 +576,24 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { }, }; break; - case YAxisType.POWER: - case YAxisType.ENERGY: - case YAxisType.REACTIVE: + case YAxisType.LEVEL: + options.scales[element.yAxisId] = { + ...baseConfig, + min: 0, + max: 3, + beginAtZero: true, + ticks: { + ...baseConfig.ticks, + stepSize: 1, + }, + }; + break; case YAxisType.VOLTAGE: case YAxisType.CURRENT: - case YAxisType.NONE: - options.scales[element.yAxisId] = baseConfig; + options.scales[element.yAxisId] = { + ...baseConfig, + beginAtZero: false, + }; break; case YAxisType.CURRENCY: options.scales[element.yAxisId] = { @@ -573,6 +604,12 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { }, }; break; + case YAxisType.POWER: + case YAxisType.ENERGY: + case YAxisType.REACTIVE: + case YAxisType.NONE: + options.scales[element.yAxisId] = baseConfig; + break; } return options; } @@ -869,7 +906,6 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { this.queryHistoricTimeseriesEnergy(this.service.historyPeriod.value.from, this.service.historyPeriod.value.to), ]) .then(([dataResponse, energyResponse]) => { - dataResponse = DateTimeUtils.normalizeTimestamps(unit, dataResponse); this.chartType = "line"; this.chartObject = this.getChartData(); diff --git a/ui/src/app/shared/components/chart/chart.constants.spec.ts b/ui/src/app/shared/components/chart/chart.constants.spec.ts index 2e38cb4cbe7..c107bc89f6c 100644 --- a/ui/src/app/shared/components/chart/chart.constants.spec.ts +++ b/ui/src/app/shared/components/chart/chart.constants.spec.ts @@ -7,9 +7,9 @@ import { ChartConstants } from "./chart.constants"; describe("Chart constants", () => { it("#calculateStepSize", () => { - expect(ChartConstants.calculateStepSize(0, 10)).toEqual(2.5); + expect(ChartConstants.calculateStepSize(0, 10)).toEqual(2); expect(ChartConstants.calculateStepSize(0, null)).toEqual(null); - expect(ChartConstants.calculateStepSize(-10, 0)).toEqual(2.5); + expect(ChartConstants.calculateStepSize(-10, 0)).toEqual(2); expect(ChartConstants.calculateStepSize(undefined, 0)).toEqual(null); // min higher than max @@ -26,9 +26,9 @@ describe("Chart constants", () => { }, ]; - expect(ChartConstants.getScaleOptions([], yAxis)).toEqual({ min: null, max: null, stepSize: null }); - expect(ChartConstants.getScaleOptions(datasets, yAxis)).toEqual({ min: 0, max: 1892, stepSize: 473 }); - expect(ChartConstants.getScaleOptions(null, yAxis)).toEqual(null); - expect(ChartConstants.getScaleOptions(null, null)).toEqual(null); + expect(ChartConstants.getScaleOptions([], yAxis, "line")).toEqual({ min: null, max: null, stepSize: null }); + expect(ChartConstants.getScaleOptions(datasets, yAxis, "line")).toEqual({ min: 62, max: 1892, stepSize: 366 }); + expect(ChartConstants.getScaleOptions(null, yAxis, "line")).toEqual({ min: null, max: null, stepSize: null }); + expect(ChartConstants.getScaleOptions(null, null, "line")).toEqual({ min: null, max: null, stepSize: null }); }); }); diff --git a/ui/src/app/shared/components/chart/chart.constants.ts b/ui/src/app/shared/components/chart/chart.constants.ts index 93bf4dc58e7..3ed4294bb78 100644 --- a/ui/src/app/shared/components/chart/chart.constants.ts +++ b/ui/src/app/shared/components/chart/chart.constants.ts @@ -1,15 +1,16 @@ // @ts-strict-ignore -import { ChartComponentLike, ChartDataset } from "chart.js"; - import { formatNumber } from "@angular/common"; import { TranslateService } from "@ngx-translate/core"; +import { ChartComponentLike, ChartDataset } from "chart.js"; import ChartDataLabels from "chartjs-plugin-datalabels"; +import { RGBColor } from "../../service/defaulttypes"; import { HistoryUtils, Utils } from "../../service/utils"; +import { Language } from "../../type/language"; import { ArrayUtils } from "../../utils/array/array.utils"; import { AbstractHistoryChart } from "./abstracthistorychart"; export class ChartConstants { - public static readonly NUMBER_OF_Y_AXIS_TICKS: number = 6; + public static readonly NUMBER_OF_Y_AXIS_TICKS: number = 7; public static readonly EMPTY_DATASETS: ChartDataset[] = []; public static readonly REQUEST_TIMEOUT = 500; @@ -41,7 +42,8 @@ export class ChartConstants { public static readonly BAR_CHART_DATALABELS = (unit: string, disable: boolean): any => ({ ...ChartDataLabels, formatter: (value, ctx) => { - return formatNumber(value, "de", "1.0-0") + "\xa0" + unit ?? null; + const locale: string = (Language.getByKey(localStorage.LANGUAGE) ?? Language.DEFAULT).i18nLocaleKey; + return formatNumber(value, locale, "1.0-0") + "\xa0" + unit; }, ...{ anchor: "end", offset: -18, align: "start", clip: false, clamp: true, @@ -51,6 +53,14 @@ export class ChartConstants { }); }; + public static Colors = class { + public static RED: string = new RGBColor(200, 0, 0).toString(); + }; + + public static readonly NumberFormat = class { + public static NO_DECIMALS: string = "1.0-0"; + }; + /** * Default yScale CartesianScaleTypeRegistry.linear * @@ -62,25 +72,36 @@ export class ChartConstants { */ public static DEFAULT_Y_SCALE_OPTIONS = (element: HistoryUtils.yAxes, translate: TranslateService, chartType: "line" | "bar", datasets: ChartDataset[], showYAxisTitle?: boolean) => { const beginAtZero: boolean = ChartConstants.isDataSeriesPositive(datasets); + const scaleOptions: ReturnType = this.getScaleOptions(datasets, element, chartType); return { title: { - text: element.customTitle ?? AbstractHistoryChart.getYAxisType(element.unit, translate, chartType), - display: showYAxisTitle, + text: element.customTitle ?? AbstractHistoryChart.getYAxisType(element.unit, translate, chartType, element.customTitle), + display: false, padding: 5, font: { size: 11, }, }, + stacked: chartType === "line" ? false : true, beginAtZero: beginAtZero, position: element.position, grid: { display: element.displayGrid ?? true, }, + ...(scaleOptions?.min !== null && { min: scaleOptions.min }), + ...(scaleOptions?.max !== null && { max: scaleOptions.max }), ticks: { color: getComputedStyle(document.documentElement).getPropertyValue("--ion-color-text"), padding: 5, maxTicksLimit: ChartConstants.NUMBER_OF_Y_AXIS_TICKS, + ...(scaleOptions?.stepSize && { stepSize: scaleOptions.stepSize }), + callback: function (value, index, ticks) { + if (index == (ticks.length - 1) && showYAxisTitle) { + return element.customTitle ?? AbstractHistoryChart.getYAxisType(element.unit, translate, chartType); + } + return value; + }, }, }; }; @@ -92,11 +113,36 @@ export class ChartConstants { * @param yAxis the yAxis * @returns min, max and stepsize for datasets belonging to this yAxis */ - public static getScaleOptions(datasets: ChartDataset[], yAxis: HistoryUtils.yAxes): { min: number; max: number; stepSize: number; } | null { + public static getScaleOptions(datasets: ChartDataset[], yAxis: HistoryUtils.yAxes, chartType: "line" | "bar"): { min: number; max: number; stepSize: number; } | null { + + const stackMap: { [index: string]: ChartDataset } = {}; + datasets?.filter(el => el["yAxisID"] === yAxis.yAxisId).forEach((dataset, index) => { + const stackId = dataset.stack || "default"; // If no stack is defined, use "default" + + if (dataset.hidden) { + return; + } + + if (chartType === "line") { + stackMap[index] = dataset; + return; + } + + if (!(stackId in stackMap)) { + // If the stack doesn"t exist yet, create an entry + stackMap[stackId] = { ...dataset, data: [...dataset.data] }; + } else { + // If the stack already exists, merge the data arrays + stackMap[stackId].data = stackMap[stackId].data.map((value, index) => { + return Utils.addSafely(value as number, (dataset.data[index] as number || 0)); // Sum data points or handle missing values + }); + } + }); - return datasets?.filter(el => el["yAxisID"] === yAxis.yAxisId) - .reduce((arr, dataset) => { - const min = Math.floor(Math.min(arr.min, ArrayUtils.findSmallestNumber(dataset.data as number[]))) ?? null; + return Object.values(stackMap) + .reduce((arr: { min: number, max: number, stepSize: number }, dataset: ChartDataset) => { + const currMin = ArrayUtils.findSmallestNumber(dataset.data as number[]); + const min = Math.floor(Math.min(...[arr.min, currMin].filter(el => el != null))) ?? null; const max = Math.ceil(Math.max(arr.max, ArrayUtils.findBiggestNumber(dataset.data as number[]))) ?? null; if (max === null || min === null) { @@ -106,7 +152,7 @@ export class ChartConstants { arr = { min: min, max: max, - stepSize: Math.max(arr.stepSize, ChartConstants.calculateStepSize(min, max)), + stepSize: Math.max(arr?.stepSize ?? 0, ChartConstants.calculateStepSize(min, max)), }; return arr; diff --git a/ui/src/app/shared/components/chart/chart.html b/ui/src/app/shared/components/chart/chart.html index 075411661cb..0bed497b52d 100644 --- a/ui/src/app/shared/components/chart/chart.html +++ b/ui/src/app/shared/components/chart/chart.html @@ -15,7 +15,8 @@ - +
    +
    diff --git a/ui/src/app/shared/components/components.module.ts b/ui/src/app/shared/components/components.module.ts index 8e39c2dc74e..cb3b444a6fb 100644 --- a/ui/src/app/shared/components/components.module.ts +++ b/ui/src/app/shared/components/components.module.ts @@ -9,9 +9,9 @@ import { PipeModule } from "../pipe/pipe"; import { ChartModule } from "./chart/chart.module"; import { FlatWidgetComponent } from "./flat/flat"; import { FlatWidgetHorizontalLineComponent } from "./flat/flat-widget-horizontal-line/flat-widget-horizontal-line"; -import { FlatWidgetLineDividerComponent } from "./flat/flat-widget-line-divider/flat-widget-line-divider"; import { FlatWidgetLineComponent } from "./flat/flat-widget-line/flat-widget-line"; import { FlatWidgetLineItemComponent } from "./flat/flat-widget-line/flat-widget-line-item/flat-widget-line-item"; +import { FlatWidgetLineDividerComponent } from "./flat/flat-widget-line-divider/flat-widget-line-divider"; import { FlatWidgetPercentagebarComponent } from "./flat/flat-widget-percentagebar/flat-widget-percentagebar"; import { FooterComponent } from "./footer/footer"; import { FooterNavigationModule } from "./footer/subnavigation/footerNavigation.module"; diff --git a/ui/src/app/shared/components/edge/edge.ts b/ui/src/app/shared/components/edge/edge.ts index babbb91da9e..64cb0fe962a 100644 --- a/ui/src/app/shared/components/edge/edge.ts +++ b/ui/src/app/shared/components/edge/edge.ts @@ -22,9 +22,9 @@ import { GetChannelResponse } from "../../jsonrpc/response/getChannelResponse"; import { Channel, GetChannelsOfComponentResponse } from "../../jsonrpc/response/getChannelsOfComponentResponse"; import { GetEdgeConfigResponse } from "../../jsonrpc/response/getEdgeConfigResponse"; import { GetPropertiesOfFactoryResponse } from "../../jsonrpc/response/getPropertiesOfFactoryResponse"; -import { ArrayUtils } from "../../service/arrayutils"; import { ChannelAddress, EdgePermission, SystemLog, Websocket } from "../../shared"; import { Role } from "../../type/role"; +import { ArrayUtils } from "../../utils/array/array.utils"; import { CurrentData } from "./currentdata"; import { EdgeConfig } from "./edgeconfig"; diff --git a/ui/src/app/shared/components/edge/edgeconfig.ts b/ui/src/app/shared/components/edge/edgeconfig.ts index 4b12b73aed2..907ebb27fb9 100644 --- a/ui/src/app/shared/components/edge/edgeconfig.ts +++ b/ui/src/app/shared/components/edge/edgeconfig.ts @@ -115,7 +115,7 @@ export class EdgeConfig { category: { title: "Zähler", icon: "speedometer-outline" }, factories: [ EdgeConfig.getFactoriesByNature(factories, "io.openems.edge.meter.api.SymmetricMeter"), // TODO replaced by ElectricityMeter - EdgeConfig.getFactoriesByNature(factories, "io.openems.edge.meter.api.ElectricityMeter"), + EdgeConfig.getFactoriesByNature(factories, "io.openems.edge.meter.api.ElectricityMeter", "io.openems.edge.evcs.api.Evcs"), EdgeConfig.getFactoriesByNature(factories, "io.openems.edge.ess.dccharger.api.EssDcCharger"), ].flat(2), }, @@ -258,13 +258,20 @@ export class EdgeConfig { /** * Get Factories of Nature. * - * @param natureId the given Nature. + * @param factories the given EdgeConfig.Factory + * @param includeNature the name of the Nature to be included + * @param excludeNature an optional name of a Nature to be excluded */ - public static getFactoriesByNature(factories: { [id: string]: EdgeConfig.Factory }, natureId: string): EdgeConfig.Factory[] { + public static getFactoriesByNature(factories: { [id: string]: EdgeConfig.Factory }, includeNature: string, excludeNature?: string): EdgeConfig.Factory[] { const result = []; - const nature = EdgeConfig.getNaturesOfFactories(factories)[natureId]; - if (nature) { - for (const factoryId of nature.factoryIds) { + const natures = EdgeConfig.getNaturesOfFactories(factories); + const include = natures[includeNature]; + const excludes = excludeNature != null && excludeNature in natures ? natures[excludeNature].factoryIds : []; + if (include) { + for (const factoryId of include.factoryIds) { + if (excludes.includes(factoryId)) { + continue; + } if (factoryId in factories) { result.push(factories[factoryId]); } @@ -509,25 +516,19 @@ export class EdgeConfig { if (component.properties["type"] == "PRODUCTION") { return true; } + const natureIds = this.getNatureIdsByFactoryId(component.factoryId); + if (natureIds.includes("io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter") + || natureIds.includes("io.openems.edge.ess.dccharger.api.EssDcCharger")) { + return true; + } // TODO properties in OSGi Component annotations are not transmitted correctly with Apache Felix SCR switch (component.factoryId) { case "Fenecon.Dess.PvMeter": case "Fenecon.Mini.PvMeter": case "Fenecon.Pro.PvMeter": - case "Kaco.BlueplanetHybrid10.PvInverter": - case "Kostal.Piko.Charger": - case "PV-Inverter.Fronius": - case "PV-Inverter.KACO.blueplanet": - case "PV-Inverter.Kostal": - case "PV-Inverter.SMA.SunnyTripower": - case "PV-Inverter.Solarlog": - case "PV-Inverter.SunSpec": case "Simulator.ProductionMeter.Acting": - case "Simulator.PvInverter": - case "SolarEdge.PV-Inverter": return true; } - return false; } @@ -540,11 +541,14 @@ export class EdgeConfig { public isTypeConsumptionMetered(component: EdgeConfig.Component) { if (component.properties["type"] == "CONSUMPTION_METERED") { return true; - } else { - switch (component.factoryId) { - case "GoodWe.EmergencyPowerMeter": - return true; - } + } + switch (component.factoryId) { + case "GoodWe.EmergencyPowerMeter": + return true; + } + const natures = this.getNatureIdsByFactoryId(component.factoryId); + if (natures.includes("io.openems.edge.evcs.api.Evcs") && !natures.includes("io.openems.edge.evcs.api.MetaEvcs")) { + return true; } return false; } diff --git a/ui/src/app/shared/components/edge/meter/currentVoltage/chart/asymmetricMeter.ts b/ui/src/app/shared/components/edge/meter/currentVoltage/chart/asymmetricMeter.ts index 54d5f05ee5c..f187ea50ccc 100644 --- a/ui/src/app/shared/components/edge/meter/currentVoltage/chart/asymmetricMeter.ts +++ b/ui/src/app/shared/components/edge/meter/currentVoltage/chart/asymmetricMeter.ts @@ -34,7 +34,7 @@ export class CurrentVoltageAsymmetricChartComponent extends AbstractHistoryChart }, hideShadow: true, color: currentPhasesColors[index], - yAxisId: ChartAxis.RIGHT, + yAxisId: ChartAxis.LEFT, })), ...Phase.THREE_PHASE.map((phase, index) => ({ name: this.translate.instant("Edge.History.VOLTAGE") + " " + phase, @@ -43,6 +43,7 @@ export class CurrentVoltageAsymmetricChartComponent extends AbstractHistoryChart }, hideShadow: true, color: voltagePhasesColors[index], + yAxisId: ChartAxis.RIGHT, })), ], tooltip: { @@ -51,13 +52,14 @@ export class CurrentVoltageAsymmetricChartComponent extends AbstractHistoryChart }, yAxes: [{ unit: YAxisType.VOLTAGE, - position: "left", - yAxisId: ChartAxis.LEFT, + position: "right", + yAxisId: ChartAxis.RIGHT, + displayGrid: false, }, { unit: YAxisType.CURRENT, - position: "right", - yAxisId: ChartAxis.RIGHT, + position: "left", + yAxisId: ChartAxis.LEFT, }, ], }; diff --git a/ui/src/app/shared/components/edge/meter/currentVoltage/chart/symmetricMeter.ts b/ui/src/app/shared/components/edge/meter/currentVoltage/chart/symmetricMeter.ts index e7998dda9f6..d3ad8c86502 100644 --- a/ui/src/app/shared/components/edge/meter/currentVoltage/chart/symmetricMeter.ts +++ b/ui/src/app/shared/components/edge/meter/currentVoltage/chart/symmetricMeter.ts @@ -35,7 +35,7 @@ export class CurrentVoltageSymmetricChartComponent extends AbstractHistoryChart hiddenOnInit: false, stack: 1, - yAxisId: ChartAxis.RIGHT, + yAxisId: ChartAxis.LEFT, }, { name: this.translate.instant("Edge.History.VOLTAGE"), @@ -45,7 +45,7 @@ export class CurrentVoltageSymmetricChartComponent extends AbstractHistoryChart color: "rgb(255,0,0)", hiddenOnInit: false, stack: 1, - yAxisId: ChartAxis.LEFT, + yAxisId: ChartAxis.RIGHT, }, ], tooltip: { @@ -54,13 +54,14 @@ export class CurrentVoltageSymmetricChartComponent extends AbstractHistoryChart }, yAxes: [{ unit: YAxisType.VOLTAGE, - position: "left", - yAxisId: ChartAxis.LEFT, + position: "right", + yAxisId: ChartAxis.RIGHT, + displayGrid: false, }, { unit: YAxisType.CURRENT, - position: "right", - yAxisId: ChartAxis.RIGHT, + position: "left", + yAxisId: ChartAxis.LEFT, }, ], }; diff --git a/ui/src/app/shared/components/edge/meter/currentVoltage/currentVoltage.overview.html b/ui/src/app/shared/components/edge/meter/currentVoltage/currentVoltage.overview.html index 71727e415e6..0f1ff84692e 100644 --- a/ui/src/app/shared/components/edge/meter/currentVoltage/currentVoltage.overview.html +++ b/ui/src/app/shared/components/edge/meter/currentVoltage/currentVoltage.overview.html @@ -1,7 +1,8 @@ - + diff --git a/ui/src/app/shared/components/edge/offline/offline.component.html b/ui/src/app/shared/components/edge/offline/offline.component.html new file mode 100644 index 00000000000..4517b57f3c8 --- /dev/null +++ b/ui/src/app/shared/components/edge/offline/offline.component.html @@ -0,0 +1,6 @@ + + + + {{environment.edgeShortName}} ist offline + + diff --git a/ui/src/app/edge/live/offline/offline.component.ts b/ui/src/app/shared/components/edge/offline/offline.component.ts similarity index 78% rename from ui/src/app/edge/live/offline/offline.component.ts rename to ui/src/app/shared/components/edge/offline/offline.component.ts index 5af0358d185..2f9af23442a 100644 --- a/ui/src/app/edge/live/offline/offline.component.ts +++ b/ui/src/app/shared/components/edge/offline/offline.component.ts @@ -1,16 +1,25 @@ import { Component, OnInit } from "@angular/core"; import { Edge, Service, Utils } from "src/app/shared/shared"; +import { Role } from "src/app/shared/type/role"; import { DateUtils } from "src/app/shared/utils/date/dateutils"; +import { environment } from "src/environments"; // TODO add translations when refactoring offline.component.html @Component({ selector: "offline", templateUrl: "./offline.component.html", + styles: [` + ion-item > ion-label > h3 { + font-weight: bolder; + } + `], }) export class OfflineComponent implements OnInit { protected edge: Edge | null = null; protected timeSinceOffline: string | null = null; + protected isAtLeastInstaller: boolean = false; + protected readonly environment = environment; constructor( public service: Service, @@ -45,7 +54,8 @@ export class OfflineComponent implements OnInit { ngOnInit() { this.service.getCurrentEdge().then(edge => { this.edge = edge; - this.timeSinceOffline = OfflineComponent.formatSecondsToFullMinutes(edge.lastmessage.toString()); + this.isAtLeastInstaller = this.edge.roleIsAtLeast(Role.INSTALLER); + this.timeSinceOffline = OfflineComponent.formatSecondsToFullMinutes(edge.lastmessage?.toString()); }); } } diff --git a/ui/src/app/shared/components/edge/offline/offline.module.ts b/ui/src/app/shared/components/edge/offline/offline.module.ts new file mode 100644 index 00000000000..5c500e85c32 --- /dev/null +++ b/ui/src/app/shared/components/edge/offline/offline.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; +import { IonicModule } from "@ionic/angular"; +import { OfflineComponent } from "./offline.component"; + +@NgModule({ + imports: [ + BrowserModule, + IonicModule, + ], + declarations: [ + OfflineComponent, + ], + exports: [ + OfflineComponent, + ], +}) +export class EdgeOfflineModule { } diff --git a/ui/src/app/shared/components/flat/abstract-flat-widget-line.ts b/ui/src/app/shared/components/flat/abstract-flat-widget-line.ts index 48738330c12..04821ded125 100644 --- a/ui/src/app/shared/components/flat/abstract-flat-widget-line.ts +++ b/ui/src/app/shared/components/flat/abstract-flat-widget-line.ts @@ -4,8 +4,8 @@ import { ActivatedRoute } from "@angular/router"; import { ModalController } from "@ionic/angular"; import { Subject } from "rxjs"; import { takeUntil } from "rxjs/operators"; -import { ChannelAddress, Edge, Service, Websocket } from "src/app/shared/shared"; import { v4 as uuidv4 } from "uuid"; +import { ChannelAddress, Edge, Service, Websocket } from "src/app/shared/shared"; import { DataService } from "../shared/dataservice"; import { Filter } from "../shared/filter"; @@ -30,7 +30,10 @@ export abstract class AbstractFlatWidgetLine implements OnChanges, OnDestroy { */ public displayValue: string | null = null; + protected displayName: string = null; protected show: boolean = true; + + private _name: string | ((value: any) => string); private _channelAddress: ChannelAddress | null = null; /** @@ -48,6 +51,15 @@ export abstract class AbstractFlatWidgetLine implements OnChanges, OnDestroy { @Inject(DataService) private dataService: DataService, ) { } + @Input() set name(value: string | { channel: ChannelAddress, converter: (value: any) => string }) { + if (typeof value === "object") { + this.subscribe(value.channel); + this._name = value.converter; + } else { + this._name = value; + } + } + /** Channel defines the channel, you need for this line */ @Input() set channelAddress(channelAddress: string) { @@ -79,7 +91,14 @@ export abstract class AbstractFlatWidgetLine implements OnChanges, OnDestroy { } protected setValue(value: any) { + if (typeof this._name == "function") { + this.displayName = this._name(value); + + } else { + this.displayName = this._name; + } this.displayValue = this.converter(value); + if (this.filter) { this.show = this.filter(value); } diff --git a/ui/src/app/shared/components/flat/abstract-flat-widget.ts b/ui/src/app/shared/components/flat/abstract-flat-widget.ts index bddb7f5283e..a8a1e654f82 100644 --- a/ui/src/app/shared/components/flat/abstract-flat-widget.ts +++ b/ui/src/app/shared/components/flat/abstract-flat-widget.ts @@ -1,14 +1,14 @@ // @ts-strict-ignore import { Directive, Inject, Input, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { ModalController } from "@ionic/angular"; import { TranslateService } from "@ngx-translate/core"; import { Subject } from "rxjs"; import { takeUntil } from "rxjs/operators"; -import { ChannelAddress, CurrentData, Edge, EdgeConfig, Utils } from "src/app/shared/shared"; import { v4 as uuidv4 } from "uuid"; +import { ChannelAddress, CurrentData, Edge, EdgeConfig, Utils } from "src/app/shared/shared"; -import { FormBuilder, FormGroup } from "@angular/forms"; import { Service } from "../../service/service"; import { Websocket } from "../../service/websocket"; import { Converter } from "../shared/converter"; diff --git a/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line-item/flat-widget-line-item.html b/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line-item/flat-widget-line-item.html index 7ac84753d40..a33df0902d6 100644 --- a/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line-item/flat-widget-line-item.html +++ b/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line-item/flat-widget-line-item.html @@ -1,4 +1,5 @@ -

    diff --git a/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line.html b/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line.html index 46424574ca4..4be476c385b 100644 --- a/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line.html +++ b/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line.html @@ -2,10 +2,10 @@ - diff --git a/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line.ts b/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line.ts index 288f0f5aced..e9d3d45fdc3 100644 --- a/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line.ts +++ b/ui/src/app/shared/components/flat/flat-widget-line/flat-widget-line.ts @@ -7,9 +7,6 @@ import { AbstractFlatWidgetLine } from "../abstract-flat-widget-line"; templateUrl: "./flat-widget-line.html", }) export class FlatWidgetLineComponent extends AbstractFlatWidgetLine { - /** Name for parameter, displayed on the left side */ - @Input({ required: true }) - public name!: string; /** Width of left Column, right Column is (100 - width of left Column) */ @Input() diff --git a/ui/src/app/shared/components/flat/flat-widget-percentagebar/flat-widget-percentagebar.html b/ui/src/app/shared/components/flat/flat-widget-percentagebar/flat-widget-percentagebar.html index 5570abc71b8..4e42eb338d3 100644 --- a/ui/src/app/shared/components/flat/flat-widget-percentagebar/flat-widget-percentagebar.html +++ b/ui/src/app/shared/components/flat/flat-widget-percentagebar/flat-widget-percentagebar.html @@ -1,10 +1,10 @@ - + - {{ displayValue | unitvalue: '%' }} + {{ displayPercent | unitvalue: '%' }} diff --git a/ui/src/app/shared/components/flat/flat-widget-percentagebar/flat-widget-percentagebar.ts b/ui/src/app/shared/components/flat/flat-widget-percentagebar/flat-widget-percentagebar.ts index 668caef9e60..439c8cbd220 100644 --- a/ui/src/app/shared/components/flat/flat-widget-percentagebar/flat-widget-percentagebar.ts +++ b/ui/src/app/shared/components/flat/flat-widget-percentagebar/flat-widget-percentagebar.ts @@ -5,4 +5,11 @@ import { AbstractFlatWidgetLine } from "../abstract-flat-widget-line"; selector: "oe-flat-widget-percentagebar", templateUrl: "./flat-widget-percentagebar.html", }) -export class FlatWidgetPercentagebarComponent extends AbstractFlatWidgetLine { } +export class FlatWidgetPercentagebarComponent extends AbstractFlatWidgetLine { + + protected get displayPercent(): number | null { + return this.displayValue === null + ? null + : Math.round(Number.parseFloat(this.displayValue)); + } +} diff --git a/ui/src/app/shared/components/flat/flat-widget.component.html b/ui/src/app/shared/components/flat/flat-widget.component.html index a969eb76a61..6736d32e9d8 100644 --- a/ui/src/app/shared/components/flat/flat-widget.component.html +++ b/ui/src/app/shared/components/flat/flat-widget.component.html @@ -2,8 +2,8 @@ - - + + diff --git a/ui/src/app/shared/components/flat/flat.html b/ui/src/app/shared/components/flat/flat.html index 0e2ae2de349..11e8fcd6f82 100644 --- a/ui/src/app/shared/components/flat/flat.html +++ b/ui/src/app/shared/components/flat/flat.html @@ -2,8 +2,8 @@ - - + + diff --git a/ui/src/app/shared/components/flat/flat.ts b/ui/src/app/shared/components/flat/flat.ts index a2642ad0b75..69bf1077329 100644 --- a/ui/src/app/shared/components/flat/flat.ts +++ b/ui/src/app/shared/components/flat/flat.ts @@ -1,6 +1,6 @@ // @ts-strict-ignore import { Component, Input } from "@angular/core"; -import { Icon } from "src/app/shared/type/widget"; +import { Icon, ImageIcon } from "src/app/shared/type/widget"; @Component({ selector: "oe-flat-widget", @@ -9,7 +9,7 @@ import { Icon } from "src/app/shared/type/widget"; export class FlatWidgetComponent { /** Image in Header */ - @Input() public img?: string; + @Input() public img?: ImageIcon; /** Icon in Header */ @Input() public icon: Icon | null = null; diff --git a/ui/src/app/shared/components/formly/form-field-multi-step/form-field-multi-step.html b/ui/src/app/shared/components/formly/form-field-multi-step/form-field-multi-step.html new file mode 100644 index 00000000000..07dbace6ba8 --- /dev/null +++ b/ui/src/app/shared/components/formly/form-field-multi-step/form-field-multi-step.html @@ -0,0 +1,66 @@ + + + + + + {{props.label}} + + + + + + + {{props.description}} + + + + + + + + + + +
    + {{props.attributes?.infoLine }} +
    +
    +
    + + + + {{step.label}} + + + + + + + + + + {{step.description}} + + + + + + + + + +
    + + + + + + +
    +
    +
    +
    +
    diff --git a/ui/src/app/shared/components/formly/form-field-multi-step/form-field-multi-step.ts b/ui/src/app/shared/components/formly/form-field-multi-step/form-field-multi-step.ts new file mode 100644 index 00000000000..b9705f87493 --- /dev/null +++ b/ui/src/app/shared/components/formly/form-field-multi-step/form-field-multi-step.ts @@ -0,0 +1,70 @@ +import { Component, OnInit } from "@angular/core"; +import { FieldType } from "@ngx-formly/core"; + +@Component({ + selector: "form-field-multi-step", + templateUrl: "./form-field-multi-step.html", +}) +export class FormlyFieldMultiStepComponent extends FieldType implements OnInit { + + public currentStep: number = 0; + + protected get steps() { + return this.props.steps || []; + } + + public ngOnInit() { + // Ensure the model has an array to track steps + const stepArray = this.formControl.value; + + if (!Array.isArray(stepArray)) { + this.formControl.setValue(Array(this.steps.length).fill(false)); + } + + // Listen to status changes to reset steps if disabled + this.formControl.statusChanges.subscribe(status => { + if (status === "DISABLED") { + this.resetSteps(); + } + }); + + // Determine the current step based on the array of steps + const lastFalseIndex = stepArray.lastIndexOf(false); + const lastTrueIndex = stepArray.lastIndexOf(true); + + if (lastFalseIndex === -1) { + // All steps are true, show the final step + this.currentStep = this.steps.length - 1; + } else if (lastTrueIndex === -1) { + // No true steps, show the first step + this.currentStep = 0; + } else { + // Show the last true step + this.currentStep = lastTrueIndex; + } + } + + + protected nextStep() { + if (this.currentStep < this.steps.length - 1) { + this.currentStep++; + } + } + + protected prevStep() { + if (this.currentStep > 0) { + this.currentStep--; + } + } + + protected onCheckboxChange(event: any, index: number) { + const updatedValue = this.formControl.value; + updatedValue[index] = event.detail.checked; + this.formControl.setValue(updatedValue); + } + + private resetSteps() { + this.formControl.setValue(Array(this.steps.length).fill(false)); + this.currentStep = 0; + } +} diff --git a/ui/src/app/shared/components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.html b/ui/src/app/shared/components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.html index bd1c0ab4da9..64274e6732d 100644 --- a/ui/src/app/shared/components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.html +++ b/ui/src/app/shared/components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.html @@ -4,13 +4,20 @@ {{props.label}} - - + + + + {{props.description}} + + + + diff --git a/ui/src/app/shared/components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.ts b/ui/src/app/shared/components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.ts index c00fdafb5a2..b182f88a0c7 100644 --- a/ui/src/app/shared/components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.ts +++ b/ui/src/app/shared/components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.ts @@ -12,7 +12,15 @@ export class FormlyFieldCheckboxWithImageComponent extends FieldWrapper implemen public ngOnInit() { // If the default value is not set in beginning. - this.value = this.field.defaultValue; + this.value = this.formControl.value ?? this.field.defaultValue; + + // Listen to form control status changes to reset steps if disabled + this.formControl.statusChanges.subscribe(status => { + if (status === "DISABLED" && this.value !== false) { + this.value = false; + this.formControl.setValue(this.value); + } + }); } /** @@ -23,4 +31,13 @@ export class FormlyFieldCheckboxWithImageComponent extends FieldWrapper implemen this.formControl.setValue(this.value); } + /** + * Returns the show/hide value based on the properties. + * + * @returns boolean value representing "show" or "hide". + */ + protected showContent() { + return (!this.field.props?.disabled && !this.value) && this.field.props?.url !== undefined; + } + } diff --git a/ui/src/app/shared/components/formly/formly-skeleton-wrapper.ts b/ui/src/app/shared/components/formly/formly-skeleton-wrapper.ts index 8dc332dcf29..5677e652ae9 100644 --- a/ui/src/app/shared/components/formly/formly-skeleton-wrapper.ts +++ b/ui/src/app/shared/components/formly/formly-skeleton-wrapper.ts @@ -10,7 +10,7 @@ import { FormlyFieldConfig } from "@ngx-formly/core"; * @input model the model */ @Component({ - selector: "formly-skeleton-wrapper", + selector: "oe-formly-skeleton-wrapper", template: `
    diff --git a/ui/src/app/shared/components/header/app-header.ts b/ui/src/app/shared/components/header/app-header.ts new file mode 100644 index 00000000000..1a3a2596fbc --- /dev/null +++ b/ui/src/app/shared/components/header/app-header.ts @@ -0,0 +1,222 @@ +// @ts-strict-ignore +import { AfterViewChecked, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { NavigationEnd, Router } from "@angular/router"; +import { MenuController, ModalController } from "@ionic/angular"; +import { Subject } from "rxjs"; +import { filter, takeUntil } from "rxjs/operators"; +import { environment } from "src/environments"; + +import { Edge, Service, Websocket } from "../../shared"; +import { PickDateComponent } from "../pickdate/pickdate.component"; +import { StatusSingleComponent } from "../status/single/status.component"; + +@Component({ + selector: "app-header", + templateUrl: "./header.component.html", +}) +export class AppHeaderComponent implements OnInit, OnDestroy, AfterViewChecked { + + @ViewChild(PickDateComponent, { static: false }) public PickDateComponent: PickDateComponent; + + public environment = environment; + public backUrl: string | boolean = "/"; + public enableSideMenu: boolean; + public currentPage: "EdgeSettings" | "Other" | "IndexLive" | "IndexHistory" = "Other"; + public isSystemLogEnabled: boolean = false; + + protected isHeaderAllowed: boolean = false; + + private ngUnsubscribe: Subject = new Subject(); + private _customBackUrl: string | null = null; + + constructor( + private cdRef: ChangeDetectorRef, + public menu: MenuController, + public modalCtrl: ModalController, + public router: Router, + public service: Service, + public websocket: Websocket, + ) { } + + @Input() public set customBackUrl(url: string | null) { + if (!url) { + return; + } + this._customBackUrl = url; + this.updateBackUrl(url); + } + + ngOnInit() { + // set inital URL + this.updateUrl(this.router.routerState.snapshot.url); + // update backUrl on navigation events + this.router.events.pipe( + takeUntil(this.ngUnsubscribe), + filter(event => event instanceof NavigationEnd), + ).subscribe(event => { + window.scrollTo(0, 0); + this.updateUrl((event).urlAfterRedirects); + }); + + } + + // used to prevent 'Expression has changed after it was checked' error + ngAfterViewChecked() { + this.cdRef.detectChanges(); + } + + updateUrl(url: string) { + this.updateBackUrl(url); + this.updateEnableSideMenu(url); + this.updateCurrentPage(url); + this.isHeaderAllowed = this.isAllowedForView(url); + } + + updateEnableSideMenu(url: string) { + const urlArray = url.split("/"); + const file = urlArray.pop(); + + if (file == "user" || file == "settings" || file == "changelog" || file == "login" || file == "index" || urlArray.length > 3) { + // disable side-menu; show back-button instead + this.enableSideMenu = false; + } else { + // enable side-menu if back-button is not needed + this.enableSideMenu = true; + } + } + + updateBackUrl(url: string) { + + if (this._customBackUrl) { + this.backUrl = this._customBackUrl; + return; + } + + // disable backUrl & Segment Navigation on initial 'login' page + if (url === "/login" || url === "/overview" || url === "/index") { + this.backUrl = false; + return; + } + + + // set backUrl for user when an Edge had been selected before + const currentEdge: Edge = this.service.currentEdge.value; + if (url === "/user" && currentEdge != null) { + this.backUrl = "/device/" + currentEdge.id + "/live"; + return; + } + + // set backUrl for user if no edge had been selected + if (url === "/user") { + this.backUrl = "/overview"; + return; + } + + if (url === "/changelog" && currentEdge != null) { + // TODO this does not work if Changelog was opened from /user + this.backUrl = "/device/" + currentEdge.id + "/settings/profile"; + return; + } + + const urlArray = url.split("/"); + let backUrl: string | boolean = "/"; + const file = urlArray.pop(); + + // disable backUrl for History & EdgeIndex Component ++ Enable Segment Navigation + if ((file == "history" || file == "live") && urlArray.length == 3) { + this.backUrl = false; + return; + } + + // disable backUrl to first 'index' page from Edge index if there is only one Edge in the system + if (file === "live" && urlArray.length == 3 && this.environment.backend === "OpenEMS Edge") { + this.backUrl = false; + return; + } + + // remove one part of the url for 'index' + if (file === "live") { + urlArray.pop(); + } + + // fix url for App "settings/app/install" and "settings/app/update" + if (urlArray.slice(-3, -1).join("/") === "settings/app") { + urlArray.pop(); + } + + // re-join the url + backUrl = urlArray.join("/") || "/"; + + // correct path for '/device/[edgeId]/index' + if (backUrl === "/device") { + backUrl = "/"; + } + this.backUrl = backUrl; + } + + updateCurrentPage(url: string) { + const urlArray = url.split("/"); + let file = urlArray.pop(); + if (urlArray.length >= 4) { + file = urlArray[3]; + } + // Enable Segment Navigation for Edge-Index-Page + if ((file == "history" || file == "live") && urlArray.length == 3) { + if (file == "history") { + this.currentPage = "IndexHistory"; + } else { + this.currentPage = "IndexLive"; + } + } else if (file == "settings" && urlArray.length > 1) { + this.currentPage = "EdgeSettings"; + } + else { + this.currentPage = "Other"; + } + } + + public segmentChanged(event) { + if (event.detail.value == "IndexLive") { + this.router.navigate(["/device/" + this.service.currentEdge.value.id + "/live"], { replaceUrl: true }); + this.cdRef.detectChanges(); + } + if (event.detail.value == "IndexHistory") { + + /** Creates bug of being infinite forwarded betweeen live and history, if not relatively routed */ + // this.router.navigate(["../history"], { relativeTo: this.route }); + this.router.navigate(["/device/" + this.service.currentEdge.value.id + "/history"]); + this.cdRef.detectChanges(); + } + } + + async presentSingleStatusModal() { + const modal = await this.modalCtrl.create({ + component: StatusSingleComponent, + }); + return await modal.present(); + } + + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + private isAllowedForView(url: string): boolean { + + // Strip queryParams + const cleanUrl = url.split("?")[0]; + + if (url.includes("/history/")) { + return false; + } + + switch (cleanUrl) { + case "/login": + case "/index": + case "/demo": + return false; + default: + return true; + } + } +} diff --git a/ui/src/app/shared/components/header/header.component.html b/ui/src/app/shared/components/header/header.component.html index b1e4714c831..218cce9a898 100644 --- a/ui/src/app/shared/components/header/header.component.html +++ b/ui/src/app/shared/components/header/header.component.html @@ -1,4 +1,4 @@ - + @@ -19,33 +19,32 @@ - - - - General.LIVE - - - General.HISTORY - - - + + + General.LIVE + + + General.HISTORY + + - + + [name]="edge.roleIsAtLeast('admin') ? 'information-outline' : 'checkmark-circle-outline'" + size="medium"> diff --git a/ui/src/app/shared/components/header/header.component.ts b/ui/src/app/shared/components/header/header.component.ts index fc97026c4e6..1a4fe4dbf74 100644 --- a/ui/src/app/shared/components/header/header.component.ts +++ b/ui/src/app/shared/components/header/header.component.ts @@ -1,6 +1,6 @@ // @ts-strict-ignore -import { AfterViewChecked, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; +import { AfterViewChecked, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { NavigationEnd, Router } from "@angular/router"; import { MenuController, ModalController } from "@ionic/angular"; import { Subject } from "rxjs"; import { filter, takeUntil } from "rxjs/operators"; @@ -23,7 +23,11 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewChecked { public enableSideMenu: boolean; public currentPage: "EdgeSettings" | "Other" | "IndexLive" | "IndexHistory" = "Other"; public isSystemLogEnabled: boolean = false; + + protected isHeaderAllowed: boolean = true; + private ngUnsubscribe: Subject = new Subject(); + private _customBackUrl: string | null = null; constructor( private cdRef: ChangeDetectorRef, @@ -32,9 +36,16 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewChecked { public router: Router, public service: Service, public websocket: Websocket, - private route: ActivatedRoute, ) { } + @Input() public set customBackUrl(url: string | null) { + if (!url) { + return; + } + this._customBackUrl = url; + this.updateBackUrl(url); + } + ngOnInit() { // set inital URL this.updateUrl(this.router.routerState.snapshot.url); @@ -46,6 +57,7 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewChecked { window.scrollTo(0, 0); this.updateUrl((event).urlAfterRedirects); }); + } // used to prevent 'Expression has changed after it was checked' error @@ -74,6 +86,11 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewChecked { updateBackUrl(url: string) { + if (this._customBackUrl) { + this.backUrl = this._customBackUrl; + return; + } + // disable backUrl & Segment Navigation on initial 'login' page if (url === "/login" || url === "/overview" || url === "/index") { this.backUrl = false; @@ -165,7 +182,8 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewChecked { if (event.detail.value == "IndexHistory") { /** Creates bug of being infinite forwarded betweeen live and history, if not relatively routed */ - this.router.navigate(["../history"], { relativeTo: this.route }); + // this.router.navigate(["../history"], { relativeTo: this.route }); + this.router.navigate(["/device/" + this.service.currentEdge.value.id + "/history"]); this.cdRef.detectChanges(); } } diff --git a/ui/src/app/shared/components/modal/abstract-modal-line.ts b/ui/src/app/shared/components/modal/abstract-modal-line.ts index f16f75a48f8..0db855672ea 100644 --- a/ui/src/app/shared/components/modal/abstract-modal-line.ts +++ b/ui/src/app/shared/components/modal/abstract-modal-line.ts @@ -6,8 +6,8 @@ import { ModalController } from "@ionic/angular"; import { TranslateService } from "@ngx-translate/core"; import { Subject } from "rxjs"; import { takeUntil } from "rxjs/operators"; -import { ChannelAddress, CurrentData, Edge, EdgeConfig, Service, Utils, Websocket } from "src/app/shared/shared"; import { v4 as uuidv4 } from "uuid"; +import { ChannelAddress, CurrentData, Edge, EdgeConfig, Service, Utils, Websocket } from "src/app/shared/shared"; import { Role } from "../../type/role"; import { Converter } from "../shared/converter"; diff --git a/ui/src/app/shared/components/modal/abstractModal.ts b/ui/src/app/shared/components/modal/abstractModal.ts index 225cf314e76..b6520994122 100644 --- a/ui/src/app/shared/components/modal/abstractModal.ts +++ b/ui/src/app/shared/components/modal/abstractModal.ts @@ -6,8 +6,8 @@ import { ModalController } from "@ionic/angular"; import { TranslateService } from "@ngx-translate/core"; import { Subject, Subscription } from "rxjs"; import { takeUntil } from "rxjs/operators"; -import { ChannelAddress, CurrentData, Edge, EdgeConfig, Service, Utils, Websocket } from "src/app/shared/shared"; import { v4 as uuidv4 } from "uuid"; +import { ChannelAddress, CurrentData, Edge, EdgeConfig, Service, Utils, Websocket } from "src/app/shared/shared"; import { Role } from "../../type/role"; import { Converter } from "../shared/converter"; diff --git a/ui/src/app/shared/components/shared/converter.ts b/ui/src/app/shared/components/shared/converter.ts index b76ad1ace3f..6791bf9166e 100644 --- a/ui/src/app/shared/components/shared/converter.ts +++ b/ui/src/app/shared/components/shared/converter.ts @@ -93,6 +93,20 @@ export namespace Converter { Formatter.FORMAT_WATT(value)); }; + /** + * Formats a Power value as Watt [W]. + * + * Value 1000 -> "1.000 W". + * Value null -> "-". + * + * @param value the power value + * @returns formatted value; '-' for null + */ + export const POWER_IN_KILO_WATT: Converter = (raw) => { + return IF_NUMBER(raw, value => + Formatter.FORMAT_KILO_WATT(Utils.divideSafely(value, 1000))); + }; + /** * Formats a Energy value as Kilo watt hours [kWh]. * diff --git a/ui/src/app/shared/components/shared/formatter.ts b/ui/src/app/shared/components/shared/formatter.ts index 798818e17b6..8cfba6bba0b 100644 --- a/ui/src/app/shared/components/shared/formatter.ts +++ b/ui/src/app/shared/components/shared/formatter.ts @@ -1,39 +1,54 @@ import { formatNumber } from "@angular/common"; import { Currency } from "../../shared"; +import { Language } from "../../type/language"; export namespace Formatter { + + // Changes the number format based on the language selected. + const locale: string = (Language.getByKey(localStorage.LANGUAGE) ?? Language.DEFAULT).i18nLocaleKey; + export const FORMAT_WATT = (value: number) => { // TODO apply correct locale - return formatNumber(value, "de", "1.0-0") + " W"; + return formatNumber(value, locale, "1.0-0") + " W"; + }; + + export const FORMAT_KILO_WATT = (value: number) => { + // TODO apply correct locale + return formatNumber(value, locale, "1.0-2") + " kW"; }; export const FORMAT_KILO_WATT_HOURS = (value: number) => { // TODO apply correct locale - return formatNumber(value, "de", "1.0-0") + " kWh"; + return formatNumber(value, locale, "1.0-0") + " kWh"; }; export const FORMAT_VOLT = (value: number) => { // TODO apply correct locale - return formatNumber(value, "de", "1.0-0") + " V"; + return formatNumber(value, locale, "1.0-0") + " V"; }; export const FORMAT_AMPERE = (value: number) => { // TODO apply correct locale - return formatNumber(value, "de", "1.1-1") + " A"; + return formatNumber(value, locale, "1.1-1") + " A"; }; export const FORMAT_CELSIUS = (value: number) => { // TODO apply correct locale - return formatNumber(value, "de", "1.0-0") + " °C"; + return formatNumber(value, locale, "1.0-0") + " °C"; }; export const FORMAT_PERCENT = (value: number) => { // TODO apply correct locale - return formatNumber(value, "de", "1.0-0") + " %"; + return formatNumber(value, locale, "1.0-0") + " %"; + }; + + export const FORMAT_BAR = (value: number) => { + // TODO apply correct locale + return formatNumber(value, locale, "1.1-1") + " mbar"; }; export const FORMAT_CURRENCY_PER_KWH = (value: number | string, currency: string = Currency.Unit.CENT) => { // TODO apply correct locale - return formatNumber(parseInt(value.toString()), "de", "1.0-2") + " " + Currency.getCurrencyLabelByCurrency(currency); + return formatNumber(parseInt(value.toString()), locale, "1.0-2") + " " + Currency.getCurrencyLabelByCurrency(currency); }; } diff --git a/ui/src/app/shared/components/shared/testing/common.ts b/ui/src/app/shared/components/shared/testing/common.ts index 62f21fe345a..ea9af4c8948 100644 --- a/ui/src/app/shared/components/shared/testing/common.ts +++ b/ui/src/app/shared/components/shared/testing/common.ts @@ -22,15 +22,16 @@ export namespace OeTester { } export namespace ChartOptions { - export const LINE_CHART_OPTIONS = (period: string, chartType: "line" | "bar", options: { [key: string]: { scale: { min?: number, max?: number, beginAtZero?: boolean }, ticks?: { stepSize: number; }; }; }, title?: string): OeChartTester.Dataset.Option => ({ + export const LINE_CHART_OPTIONS = (period: string, chartType: "line" | "bar", options: { [key: string]: { scale: { min?: number, max?: number, beginAtZero?: boolean }, ticks?: { stepSize: number; min?: number, max?: number }; }; }, title?: string): OeChartTester.Dataset.Option => ({ type: "option", options: { "responsive": true, "maintainAspectRatio": false, "elements": { "point": { "radius": 0, "hitRadius": 0, "hoverRadius": 0 }, "line": { "stepped": false, "fill": true } }, "datasets": { "bar": {}, "line": {} }, "plugins": { - "colors": { "enabled": false }, "legend": { "display": true, "position": "bottom", "labels": { "color": "" } }, "tooltip": { "intersect": false, "mode": "index", "callbacks": {} }, "annotation": { "annotations": {} }, "datalabels": { + "colors": { "enabled": false }, "legend": { "display": true, "position": "bottom", "labels": { "color": "" } }, "tooltip": { "intersect": false, "mode": "index", "callbacks": {}, "enabled": true }, "annotation": { "annotations": {} }, "datalabels": { display: false, }, }, "scales": { "x": { "stacked": true, "offset": false, "type": "time", "ticks": { "source": "auto", "maxTicksLimit": 31 }, "bounds": "ticks", "adapters": { "date": { "locale": { "code": "de", "formatLong": {}, "localize": {}, "match": {}, "options": { "weekStartsOn": 1, "firstWeekContainsDate": 4 } } } }, "time": { "unit": period as TimeUnit, "displayFormats": { "datetime": "yyyy-MM-dd HH:mm:ss", "millisecond": "SSS [ms]", "second": "HH:mm:ss a", "minute": "HH:mm", "hour": "HH:00", "day": "dd", "week": "ll", "month": "MM", "quarter": "[Q]Q - YYYY", "year": "yyyy" } } }, "left": { + "stacked": false, "beginAtZero": false, ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "title": { "text": "kW", "display": false, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, "ticks": { ...options["left"]?.ticks, @@ -46,13 +47,15 @@ export namespace OeTester { type: "option", options: { "responsive": true, "maintainAspectRatio": false, "elements": { "point": { "radius": 0, "hitRadius": 0, "hoverRadius": 0 }, "line": { "stepped": false, "fill": true } }, "datasets": { "bar": { "barPercentage": 1 }, "line": {} }, "plugins": { - "colors": { "enabled": false }, "legend": { "display": true, "position": "bottom", "labels": { "color": "" } }, "tooltip": { "intersect": false, "mode": "x", "callbacks": {} }, "annotation": { "annotations": {} }, "datalabels": { + "colors": { "enabled": false }, "legend": { "display": true, "position": "bottom", "labels": { "color": "" } }, "tooltip": { "intersect": false, "mode": "x", "callbacks": {}, "enabled": true }, "annotation": { "annotations": {} }, "datalabels": { display: false, }, }, "scales": { "x": { "stacked": true, "offset": true, "type": "time", "ticks": { "source": "auto", "maxTicksLimit": 31 }, "bounds": "ticks", "adapters": { "date": { "locale": { "code": "de", "formatLong": {}, "localize": {}, "match": {}, "options": { "weekStartsOn": 1, "firstWeekContainsDate": 4 } } } }, "time": { "unit": period as TimeUnit, "displayFormats": { "datetime": "yyyy-MM-dd HH:mm:ss", "millisecond": "SSS [ms]", "second": "HH:mm:ss a", "minute": "HH:mm", "hour": "HH:00", "day": "dd", "week": "ll", "month": "MM", "quarter": "[Q]Q - YYYY", "year": "yyyy" } } }, "left": { - ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "title": { "text": "kWh", "display": false, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, + "stacked": true, + ...options["left"]?.scale, + ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "title": { "text": "kWh", "display": false, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, "ticks": { ...options["left"]?.ticks, "color": "", @@ -75,6 +78,7 @@ export namespace OeTester { "display": true, "position": "bottom", "labels": { "color": "" }, }, "tooltip": { "intersect": false, "mode": "index", "callbacks": {}, + "enabled": true, }, "annotation": { "annotations": {}, @@ -84,8 +88,9 @@ export namespace OeTester { }, }, "scales": { "x": { "stacked": true, "offset": false, "type": "time", "ticks": { "source": "auto", "maxTicksLimit": 31 }, "bounds": "ticks", "adapters": { "date": { "locale": { "code": "de", "formatLong": {}, "localize": {}, "match": {}, "options": { "weekStartsOn": 1, "firstWeekContainsDate": 4 } } } }, "time": { "unit": period as TimeUnit, "displayFormats": { "datetime": "yyyy-MM-dd HH:mm:ss", "millisecond": "SSS [ms]", "second": "HH:mm:ss a", "minute": "HH:mm", "hour": "HH:00", "day": "dd", "week": "ll", "month": "MM", "quarter": "[Q]Q - YYYY", "year": "yyyy" } } }, "left": { + "stacked": false, ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, - "title": { "text": "kW", "display": true, "padding": 5, "font": { "size": 11 } }, + "title": { "text": "kW", "display": false, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, "ticks": { ...options["left"]?.ticks, @@ -95,8 +100,9 @@ export namespace OeTester { }, }, "right": { + "stacked": false, ...options["right"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, - "title": { "text": "Zustand", "display": true, "padding": 5, "font": { "size": 11 } }, + "title": { "text": "Zustand", "display": false, "padding": 5, "font": { "size": 11 } }, "position": "right", "grid": { "display": false }, "ticks": { ...options["right"]?.ticks, @@ -112,13 +118,14 @@ export namespace OeTester { type: "option", options: { "responsive": true, "maintainAspectRatio": false, "elements": { "point": { "radius": 0, "hitRadius": 0, "hoverRadius": 0 }, "line": { "stepped": false, "fill": true } }, "datasets": { "bar": { "barPercentage": 1 }, "line": {} }, "plugins": { - "colors": { "enabled": false }, "legend": { "display": true, "position": "bottom", "labels": { "color": "" } }, "tooltip": { "intersect": false, "mode": "x", "callbacks": {} }, "annotation": { "annotations": {} }, "datalabels": { + "colors": { "enabled": false }, "legend": { "display": true, "position": "bottom", "labels": { "color": "" } }, "tooltip": { "intersect": false, "mode": "x", "callbacks": {}, "enabled": true }, "annotation": { "annotations": {} }, "datalabels": { display: false, }, }, "scales": { "x": { "stacked": true, "offset": true, "type": "time", "ticks": { "source": "auto", "maxTicksLimit": 31 }, "bounds": "ticks", "adapters": { "date": { "locale": { "code": "de", "formatLong": {}, "localize": {}, "match": {}, "options": { "weekStartsOn": 1, "firstWeekContainsDate": 4 } } } }, "time": { "unit": period as TimeUnit, "displayFormats": { "datetime": "yyyy-MM-dd HH:mm:ss", "millisecond": "SSS [ms]", "second": "HH:mm:ss a", "minute": "HH:mm", "hour": "HH:00", "day": "dd", "week": "ll", "month": "MM", "quarter": "[Q]Q - YYYY", "year": "yyyy" } } }, "left": { - ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "title": { "text": "kWh", "display": true, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, + "stacked": true, + ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "title": { "text": "kWh", "display": false, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, "ticks": { ...options["left"]?.ticks, "color": "", @@ -127,8 +134,9 @@ export namespace OeTester { }, }, "right": { - ...options["right"]?.scale, ...(chartType === "line" ? { stacked: false } : { min: 0 }), "beginAtZero": true, - "title": { "text": "Aktive Zeit", "display": true, "padding": 5, "font": { "size": 11 } }, + "stacked": true, + ...options["right"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, + "title": { "text": "Aktive Zeit", "display": false, "padding": 5, "font": { "size": 11 } }, "position": "right", "grid": { "display": false }, "ticks": { "color": "", diff --git a/ui/src/app/shared/components/shared/testing/tester.ts b/ui/src/app/shared/components/shared/testing/tester.ts index 2e30117a4fa..9a4084f6bb5 100644 --- a/ui/src/app/shared/components/shared/testing/tester.ts +++ b/ui/src/app/shared/components/shared/testing/tester.ts @@ -6,6 +6,7 @@ import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from "src/app/shared/j import { HistoryUtils } from "src/app/shared/service/utils"; import { CurrentData, EdgeConfig } from "src/app/shared/shared"; +import { ObjectUtils } from "src/app/shared/utils/object/object.utils"; import { AbstractHistoryChart } from "../../chart/abstracthistorychart"; import { XAxisType } from "../../chart/chart.constants"; import { TextIndentation } from "../../modal/modal-line/modal-line"; @@ -233,6 +234,7 @@ export class OeChartTester { from: new Date(channelData.result.timestamps[0] ?? 0), to: new Date(channelData.result.timestamps.reverse()[0] ?? 0), getText: () => testContext.service.historyPeriod.value.getText(testContext.translate, testContext.service), + isWeekOrDay: () => testContext.service.historyPeriod.value.isWeekOrDay(), }); // Fill Data @@ -306,9 +308,17 @@ export class OeChartTester { legendOptions.push(AbstractHistoryChart.getLegendOptions(label, displayValue)); }); + const options = AbstractHistoryChart.getOptions(chartData, chartType, testContext.service, testContext.translate, legendOptions, channelData.result, locale, config, datasets, xAxisType, labels); + + chartData.yAxes.filter(axis => axis.unit != null).forEach(axis => { + // Remove custom scale calculations from unittest, seperate unittest existing + options.scales[axis.yAxisId] = ObjectUtils.excludeProperties(options.scales[axis.yAxisId], ["min", "max"]) as Chart.ScaleOptionsByType<"radialLinear" | keyof Chart.CartesianScaleTypeRegistry>; + options.scales[axis.yAxisId].ticks = ObjectUtils.excludeProperties(options.scales[axis.yAxisId].ticks as Chart.RadialTickOptions, ["stepSize"]); + }); + return { type: "option", - options: AbstractHistoryChart.getOptions(chartData, chartType, testContext.service, testContext.translate, legendOptions, channelData.result, locale, config, datasets, xAxisType, labels), + options: options, }; } diff --git a/ui/src/app/shared/jsonrpc/jsonrpcutils.spec.ts b/ui/src/app/shared/jsonrpc/jsonrpcutils.spec.ts new file mode 100644 index 00000000000..0133cd573ed --- /dev/null +++ b/ui/src/app/shared/jsonrpc/jsonrpcutils.spec.ts @@ -0,0 +1,10 @@ +import { JsonRpcUtils } from "./jsonrpcutils"; + +describe("JsonRpcUtils", () => { + + const productionActivePowerData = [-0.01, -0.1, -0.49, -0.50, -1, null]; + const expectedOutput = [0, 0, 0, -0.5, -1, null]; + it("#normalizeQueryData", () => { + expect(JsonRpcUtils.normalizeQueryData(productionActivePowerData)).toEqual(expectedOutput); + }); +}); diff --git a/ui/src/app/shared/jsonrpc/jsonrpcutils.ts b/ui/src/app/shared/jsonrpc/jsonrpcutils.ts index 642ffee60c6..979d7be8b53 100644 --- a/ui/src/app/shared/jsonrpc/jsonrpcutils.ts +++ b/ui/src/app/shared/jsonrpc/jsonrpcutils.ts @@ -2,6 +2,26 @@ import { ChannelAddress } from "../type/channeladdress"; export class JsonRpcUtils { + private static THRESHOLD: number = -0.50; + + public static normalizeQueryData(data: (number | null)[]): (number | null)[] { + return data.map(el => JsonRpcUtils.roundSlightlyNegativeValues(el)); + } + + /** + * Rounds values between 0 and -1kW to 0 + * + * @param value the value to convert + */ + public static roundSlightlyNegativeValues(value: number | null): number | null { + if (value == null) { + return null; + } + + return (value > JsonRpcUtils.THRESHOLD && value < 0) ? 0 : value; + } + + /** * Converts an array of ChannelAddresses to a string array with unique values. */ diff --git a/ui/src/app/shared/ngrx-store/states.ts b/ui/src/app/shared/ngrx-store/states.ts index 33d8f0aeb0d..c957daee5e3 100644 --- a/ui/src/app/shared/ngrx-store/states.ts +++ b/ui/src/app/shared/ngrx-store/states.ts @@ -35,7 +35,7 @@ export class AppStateTracker { protected router: Router, protected pagination: Pagination, private websocket: Websocket, - private previousRouteService: PreviousRouteService, + private routeService: PreviousRouteService, ) { if (!localStorage.getItem("AppState")) { console.log(`${AppStateTracker.LOG_PREFIX} Log deactivated`); @@ -55,15 +55,18 @@ export class AppStateTracker { * Handles navigation after authentication */ public navigateAfterAuthentication() { - const segments = this.router.routerState.snapshot.url.split("/"); - const previousUrl: string = this.previousRouteService.getPreviousUrl(); - if ((previousUrl === segments[segments.length - 1]) || previousUrl === "/") { - this.router.navigate(["./overview"]); - return; - } + this.router.navigate(["overview"]); + return; + // const segments = this.router.routerState.snapshot.url.split("/"); + // const previousUrl: string = this.routeService.getPreviousUrl(); + + // if ((previousUrl === segments[segments.length - 1]) || previousUrl === "/") { + // this.router.navigate(["./overview"]); + // return; + // } - this.router.navigate(previousUrl.split("/")); + // this.router.navigate(previousUrl.split("/")); } private startStateHandler(state: States): void { diff --git a/ui/src/app/shared/service/arrayutils.ts b/ui/src/app/shared/service/arrayutils.ts deleted file mode 100644 index 5fc4e31b654..00000000000 --- a/ui/src/app/shared/service/arrayutils.ts +++ /dev/null @@ -1,27 +0,0 @@ -export namespace ArrayUtils { - export function equalsCheck(a: T[], b: T[]) { - return a.length === b.length && - a.every((v, i) => v === b[i]); - } - - /** - * Sort arrays alphabetically, according to the string returned by fn. - * Elements for which fn returns null or undefined are sorted to the end in an undefined order. - * - * @param array to sort - * @param fn to get a string to sort by - * @returns sorted array - */ - export function sortedAlphabetically(array: Type[], fn: (arg: Type) => string): Type[] { - return array.sort((a: Type, b: Type) => { - const aVal = fn(a); - const bVal = fn(b); - if (!aVal) { - return !bVal ? 0 : 1; - } else if (!bVal) { - return -1; - } - return aVal.localeCompare(bVal, undefined, { sensitivity: "accent" }); - }); - } -} diff --git a/ui/src/app/shared/service/defaulttypes.spec.ts b/ui/src/app/shared/service/defaulttypes.spec.ts new file mode 100644 index 00000000000..b5a01f05370 --- /dev/null +++ b/ui/src/app/shared/service/defaulttypes.spec.ts @@ -0,0 +1,17 @@ +// @ts-strict-ignore +import { RGBColor } from "./defaulttypes"; + +describe("Defaulttypes", () => { + + it("#RgbColor.toString()", () => { + const black = new RGBColor(0, 0, 0); + expect(black.toString()).toEqual("rgb(0,0,0)"); + }); + + it("#RgbColor.toString() - invalid values", () => { + const allInvalid = new RGBColor(null, null, null); + expect(() => allInvalid.toString()).toThrow(Error("All values need to be valid")); + const oneInvalid = new RGBColor(0, 0, null); + expect(() => oneInvalid.toString()).toThrow(Error("All values need to be valid")); + }); +}); diff --git a/ui/src/app/shared/service/defaulttypes.ts b/ui/src/app/shared/service/defaulttypes.ts index b01b9875627..f9652fb6dbe 100644 --- a/ui/src/app/shared/service/defaulttypes.ts +++ b/ui/src/app/shared/service/defaulttypes.ts @@ -1,6 +1,6 @@ // @ts-strict-ignore import { TranslateService } from "@ngx-translate/core"; -import { endOfMonth, endOfYear, format, getDay, getMonth, getYear, isSameDay, isSameMonth, isSameYear, startOfMonth, startOfYear, subDays } from "date-fns"; +import { differenceInDays, endOfMonth, endOfYear, format, getDay, getMonth, getYear, isSameDay, isSameMonth, isSameYear, startOfMonth, startOfYear, subDays } from "date-fns"; import { QueryHistoricTimeseriesEnergyResponse } from "../jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; import { ChannelAddress, Service } from "../shared"; @@ -157,6 +157,7 @@ export namespace DefaultTypes { public to: Date = new Date(), ) { } + /** * Returns a translated weekday name. * @@ -236,6 +237,15 @@ export namespace DefaultTypes { }); } } + + /** + * Checks if current period is week or day + * + * @returns true if period is week or day, false if not + */ + public isWeekOrDay(): boolean { + return Math.abs(differenceInDays(this.to, this.from)) <= 6; + } } } @@ -246,3 +256,28 @@ export type TKeyValue = { }; /** */ export type PropType = TObj[TProp]; + +type Range = Acc["length"] extends N + ? Acc[number] + : Range; + +export type RGBValue = Range<256>; // 0 to 255 + +export class RGBColor { + private readonly red: T; + private readonly green: T; + private readonly blue: T; + + constructor(red: T, green: T, blue: T) { + this.red = red; + this.green = green; + this.blue = blue; + } + + public toString(): string { + if (this.red == null || this.green == null || this.blue == null) { + throw new Error("All values need to be valid"); + } + return `rgb(${this.red},${this.green},${this.blue})`; + } +} diff --git a/ui/src/app/shared/service/pagination.ts b/ui/src/app/shared/service/pagination.ts index f75a43c2f71..bef8d9d1e77 100644 --- a/ui/src/app/shared/service/pagination.ts +++ b/ui/src/app/shared/service/pagination.ts @@ -2,9 +2,9 @@ import { Directive } from "@angular/core"; import { Router } from "@angular/router"; import { SubscribeEdgesRequest } from "../jsonrpc/request/subscribeEdgesRequest"; +import { States } from "../ngrx-store/states"; import { ChannelAddress, Edge } from "../shared"; import { Service } from "./service"; -import { States } from "../ngrx-store/states"; @Directive() export class Pagination { diff --git a/ui/src/app/shared/service/service.ts b/ui/src/app/shared/service/service.ts index e25fa22fac3..73e2b1ea3d3 100644 --- a/ui/src/app/shared/service/service.ts +++ b/ui/src/app/shared/service/service.ts @@ -1,6 +1,6 @@ // @ts-strict-ignore import { registerLocaleData } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { Injectable, WritableSignal, signal } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ToastController } from "@ionic/angular"; import { LangChangeEvent, TranslateService } from "@ngx-translate/core"; @@ -79,7 +79,7 @@ export class Service extends AbstractService { user: User, edges: { [edgeId: string]: Edge } }> = new BehaviorSubject(null); - public currentUser: User | null = null; + public currentUser: WritableSignal = signal(null); /** * Holds the current Activated Route @@ -199,6 +199,7 @@ export class Service extends AbstractService { public onLogout() { this.currentEdge.next(null); this.metadata.next(null); + this.currentUser.set(null); this.websocket.state.set(States.NOT_AUTHENTICATED); this.router.navigate(["/login"]); } diff --git a/ui/src/app/shared/service/utils.spec.ts b/ui/src/app/shared/service/utils.spec.ts index 3034ab95019..8315e541078 100644 --- a/ui/src/app/shared/service/utils.spec.ts +++ b/ui/src/app/shared/service/utils.spec.ts @@ -1,6 +1,6 @@ // @ts-strict-ignore import { DummyConfig } from "../components/edge/edgeconfig.spec"; -import { EdgeConfig } from "../shared"; +import { Currency, EdgeConfig } from "../shared"; import { HistoryUtils, Utils } from "./utils"; describe("Utils", () => { @@ -54,4 +54,12 @@ describe("Utils", () => { const expectedResult4 = [null, null, null, 565, 560, 561, 573]; expect(Utils.calculateOtherConsumption(channelData, [], [])).toEqual(expectedResult4); }); + + it("+CONVERT_PRICE_TO_CENT_PER_KWH", () => { + const currencyLabel: string = Currency.getCurrencyLabelByEdgeId("0"); + expect(Utils.CONVERT_PRICE_TO_CENT_PER_KWH(2, currencyLabel)(0)).toEqual("0 Cent/kWh"); + expect(Utils.CONVERT_PRICE_TO_CENT_PER_KWH(2, currencyLabel)(null)).toEqual("- Cent/kWh"); + expect(Utils.CONVERT_PRICE_TO_CENT_PER_KWH(2, currencyLabel)(undefined)).toEqual("- Cent/kWh"); + expect(Utils.CONVERT_PRICE_TO_CENT_PER_KWH(2, currencyLabel)(1)).toEqual("0,1 Cent/kWh"); + }); }); diff --git a/ui/src/app/shared/service/utils.ts b/ui/src/app/shared/service/utils.ts index 9e237060345..4611de81c37 100644 --- a/ui/src/app/shared/service/utils.ts +++ b/ui/src/app/shared/service/utils.ts @@ -4,7 +4,6 @@ import { TranslateService } from "@ngx-translate/core"; import { ChartDataset } from "chart.js"; import { saveAs } from "file-saver-es"; import { DefaultTypes } from "src/app/shared/service/defaulttypes"; - import { JsonrpcResponseSuccess } from "../jsonrpc/base"; import { Base64PayloadResponse } from "../jsonrpc/response/base64PayloadResponse"; import { QueryHistoricTimeseriesEnergyResponse } from "../jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; @@ -347,9 +346,9 @@ export class Utils { */ public static convertChargeDischargePower(translate: TranslateService, power: number): { name: string, value: number } { if (power >= 0) { - return { name: translate.instant("General.dischargePower"), value: power }; + return { name: translate.instant("General.DISCHARGE"), value: power }; } else { - return { name: translate.instant("General.chargePower"), value: power * -1 }; + return { name: translate.instant("General.CHARGE"), value: power * -1 }; } } @@ -396,8 +395,8 @@ export class Utils { * @returns converted value */ public static CONVERT_PRICE_TO_CENT_PER_KWH = (decimal: number, label: string) => { - return (value: number | null): string => - (!value ? "-" : formatNumber(value / 10, "de", "1.0-" + decimal)) + " " + label; + return (value: number | null | undefined): string => + (value == null ? "-" : formatNumber(value / 10, "de", "1.0-" + decimal)) + " " + label; }; /** @@ -522,7 +521,7 @@ export class Utils { * * @param value the value to convert */ - public static roundSlightlyNegativeValues(value: number) { + public static roundSlightlyNegativeValues(value: number | null): number | null { return (value > -0.49 && value < 0) ? 0 : value; } @@ -626,16 +625,17 @@ export class Utils { } export enum YAxisType { + CURRENCY, + CURRENT, + ENERGY, + LEVEL, NONE, - POWER, PERCENTAGE, - RELAY, - ENERGY, - VOLTAGE, + POWER, REACTIVE, - CURRENT, + RELAY, TIME, - CURRENCY, + VOLTAGE, } export enum ChartAxis { @@ -763,8 +763,12 @@ export namespace HistoryUtils { /** Format of Number displayed */ formatNumber: string, afterTitle?: (stack: string) => string, + /** Defaults to true */ + enabled?: boolean, }, yAxes: yAxes[], + /** Rounds slightly negative values, defaults to false */ + normalizeOutputData?: boolean, }; export type yAxes = { diff --git a/ui/src/app/shared/service/websocket.ts b/ui/src/app/shared/service/websocket.ts index 6df2b0d368b..c3640de7e82 100644 --- a/ui/src/app/shared/service/websocket.ts +++ b/ui/src/app/shared/service/websocket.ts @@ -82,7 +82,7 @@ export class Websocket implements WebsocketInterface { // received login token -> save in cookie this.cookieService.set("token", authenticateResponse.token, { expires: 365, path: "/", sameSite: "Strict", secure: location.protocol === "https:" }); - this.service.currentUser = authenticateResponse.user; + this.service.currentUser.set(authenticateResponse.user); // Metadata this.service.metadata.next({ diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index 2ff830daf99..75cca829719 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -15,6 +15,7 @@ import { ComponentsModule } from "./components/components.module"; import { MeterModule } from "./components/edge/meter/meter.module"; import { FormlyCheckBoxHyperlinkWrapperComponent } from "./components/formly/form-field-checkbox-hyperlink/form-field-checkbox-hyperlink.wrapper"; import { FormlyWrapperDefaultValueWithCasesComponent } from "./components/formly/form-field-default-cases.wrapper"; +import { FormlyFieldMultiStepComponent } from "./components/formly/form-field-multi-step/form-field-multi-step"; import { FormlyWrapperFormFieldComponent } from "./components/formly/form-field.wrapper"; import { FormlyFieldCheckboxWithImageComponent } from "./components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image"; import { FormlyFieldModalComponent } from "./components/formly/formly-field-modal/formlyfieldmodal"; @@ -26,6 +27,7 @@ import { InputTypeComponent } from "./components/formly/input"; import { FormlyInputSerialNumberWrapperComponent as FormlyWrapperInputSerialNumber } from "./components/formly/input-serial-number-wrapper"; import { PanelWrapperComponent } from "./components/formly/panel-wrapper.component"; import { RepeatTypeComponent } from "./components/formly/repeat"; +import { AppHeaderComponent } from "./components/header/app-header"; import { HeaderComponent } from "./components/header/header.component"; import { HistoryDataErrorModule } from "./components/history-data-error/history-data-error.module"; import { PercentageBarComponent } from "./components/percentagebar/percentagebar.component"; @@ -55,20 +57,12 @@ export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { return `"${field.formControl.value}" is not a valid Subnetmask`; } - @NgModule({ imports: [ BrowserAnimationsModule, - NgChartsModule, CommonModule, + ComponentsModule, DirectiveModule, - FormsModule, - IonicModule, - NgxSpinnerModule.forRoot({ - type: "ball-clip-rotate-multiple", - }), - ReactiveFormsModule, - RouterModule, FormlyModule.forRoot({ wrappers: [ { name: "form-field", component: FormlyWrapperFormFieldComponent }, @@ -84,6 +78,7 @@ export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { types: [ { name: "input", component: InputTypeComponent }, { name: "repeat", component: RepeatTypeComponent }, + { name: "multi-step", component: FormlyFieldMultiStepComponent }, ], validators: [ { name: "ip", validation: IpValidator }, @@ -94,55 +89,61 @@ export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { { name: "subnetmask", message: SubnetmaskValidatorMessage }, ], }), - PipeModule, - ComponentsModule, - TranslateModule, + FormsModule, HistoryDataErrorModule, + IonicModule, MeterModule, + NgChartsModule, + NgxSpinnerModule.forRoot({ + type: "ball-clip-rotate-multiple", + }), + PipeModule, + ReactiveFormsModule, + RouterModule, + TranslateModule, ], declarations: [ - // components + AppHeaderComponent, ChartOptionsComponent, - HeaderComponent, - PercentageBarComponent, - // formly - InputTypeComponent, - FormlyWrapperFormFieldComponent, - RepeatTypeComponent, - FormlyWrapperInputSerialNumber, + FormlyCheckBoxHyperlinkWrapperComponent, + FormlyFieldCheckboxWithImageComponent, + FormlyFieldModalComponent, + FormlyFieldMultiStepComponent, + FormlyFieldRadioWithImageComponent, + FormlyFieldWithLoadingAnimationComponent, FormlySelectFieldExtendedWrapperComponent, FormlySelectFieldModalComponent, - FormlyFieldRadioWithImageComponent, - FormlyCheckBoxHyperlinkWrapperComponent, FormlyWrapperDefaultValueWithCasesComponent, - FormlyFieldModalComponent, + FormlyWrapperFormFieldComponent, + FormlyWrapperInputSerialNumber, + HeaderComponent, + InputTypeComponent, PanelWrapperComponent, - FormlyFieldWithLoadingAnimationComponent, - FormlyFieldCheckboxWithImageComponent, + PercentageBarComponent, + RepeatTypeComponent, ], exports: [ - // modules + AppHeaderComponent, BrowserAnimationsModule, - NgChartsModule, + ChartOptionsComponent, CommonModule, + ComponentsModule, DirectiveModule, + FormlyFieldWithLoadingAnimationComponent, FormlyIonicModule, FormlyModule, FormsModule, + HeaderComponent, + HistoryDataErrorModule, IonicModule, + MeterModule, + NgChartsModule, NgxSpinnerModule, + PercentageBarComponent, + PipeModule, ReactiveFormsModule, RouterModule, TranslateModule, - PipeModule, - ComponentsModule, - MeterModule, - HistoryDataErrorModule, - // components - ChartOptionsComponent, - HeaderComponent, - PercentageBarComponent, - FormlyFieldWithLoadingAnimationComponent, ], providers: [ AppStateTracker, diff --git a/ui/src/app/shared/type/widget.ts b/ui/src/app/shared/type/widget.ts index 5f72e25e0ce..a6f73311e93 100644 --- a/ui/src/app/shared/type/widget.ts +++ b/ui/src/app/shared/type/widget.ts @@ -47,6 +47,11 @@ export type Icon = { name: string; }; +export type ImageIcon = { + src: string; + large: boolean; +}; + export class Widget { public name: WidgetNature | WidgetFactory | string; public componentId: string; diff --git a/ui/src/app/shared/utils/array/array.utils.ts b/ui/src/app/shared/utils/array/array.utils.ts index 6026afa4579..037c4c4c4f6 100644 --- a/ui/src/app/shared/utils/array/array.utils.ts +++ b/ui/src/app/shared/utils/array/array.utils.ts @@ -19,10 +19,10 @@ export namespace ArrayUtils { /** * Finds the biggest number in a array. * null, undefined, NaN, +-Infinity are ignored in this method. - * - * @param arr the arr - * @returns a number if arr not empty, else null - */ + * + * @param arr the arr + * @returns a number if arr not empty, else null + */ export function findBiggestNumber(arr: (number | null | undefined)[]): number | null { const filteredArr = arr.filter((el): el is number => Number.isFinite(el)); return filteredArr.length > 0 ? Math.max(...filteredArr) : null; @@ -48,4 +48,15 @@ export namespace ArrayUtils { return aVal.localeCompare(bVal, undefined, { sensitivity: "accent" }); }); } + + /** + * Checks if array contains at least one of the passed strings + * + * @param strings the strings + * @param arr the array + * @returns true if arr contains at least one of the strings + */ + export function containsStrings(strings: (number | string | null)[], arr: (number | string | null)[]): boolean { + return arr.filter(el => strings.includes(el)).length > 0; + } } diff --git a/ui/src/app/shared/utils/datetime/datetime-utils.ts b/ui/src/app/shared/utils/datetime/datetime-utils.ts index 70bb0b7cc58..83cec0673be 100644 --- a/ui/src/app/shared/utils/datetime/datetime-utils.ts +++ b/ui/src/app/shared/utils/datetime/datetime-utils.ts @@ -1,6 +1,9 @@ // @ts-strict-ignore +/* eslint-disable import/no-duplicates */ +// cf. https://github.com/import-js/eslint-plugin-import/issues/1479 import { format, startOfMonth, startOfYear } from "date-fns"; import { de } from "date-fns/locale"; +/* eslint-enable import/no-duplicates */ import { ChronoUnit } from "src/app/edge/history/shared"; import { QueryHistoricTimeseriesDataResponse } from "../../jsonrpc/response/queryHistoricTimeseriesDataResponse"; diff --git a/ui/src/app/shared/utils/object/object.utils.ts b/ui/src/app/shared/utils/object/object.utils.ts new file mode 100644 index 00000000000..043a0fdb465 --- /dev/null +++ b/ui/src/app/shared/utils/object/object.utils.ts @@ -0,0 +1,8 @@ +export class ObjectUtils { + + public static excludeProperties, K extends keyof T>(obj: T, keys: K[]): Omit { + const result = { ...obj }; + keys.forEach(key => delete result[key]); + return result; + } +} diff --git a/ui/src/app/user/user.component.html b/ui/src/app/user/user.component.html index bc5370ebca3..030bf9167d8 100644 --- a/ui/src/app/user/user.component.html +++ b/ui/src/app/user/user.component.html @@ -1,4 +1,3 @@ -
    @@ -75,9 +74,9 @@ - - + + - + + @@ -139,11 +139,6 @@

    - - - - Debug-Mode -
    @@ -166,6 +161,12 @@ The language can not be changed permanently in the local online-monitoring for technical reasons. + + + + + Debug-Mode + diff --git a/ui/src/app/user/user.component.ts b/ui/src/app/user/user.component.ts index b42945d1521..a360e9741c3 100644 --- a/ui/src/app/user/user.component.ts +++ b/ui/src/app/user/user.component.ts @@ -1,5 +1,5 @@ // @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, effect } from "@angular/core"; import { FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { FormlyFieldConfig } from "@ngx-formly/core"; @@ -13,6 +13,7 @@ import { GetUserInformationResponse } from "../shared/jsonrpc/response/getUserIn import { Service, Websocket } from "../shared/shared"; import { COUNTRY_OPTIONS } from "../shared/type/country"; import { Language } from "../shared/type/language"; +import { Role } from "../shared/type/role"; type CompanyUserInformation = UserInformation & { companyName: string }; @@ -55,94 +56,31 @@ export class UserComponent implements OnInit { disabled: true, }, }]; - protected readonly companyInformationFields: FormlyFieldConfig[] = []; + protected companyInformationFields: FormlyFieldConfig[] = []; + + protected isAtLeastAdmin: boolean = false; constructor( public translate: TranslateService, public service: Service, private route: ActivatedRoute, private websocket: Websocket, - ) { } + ) { + effect(() => { + const user = this.service.currentUser(); + + if (user) { + this.isAtLeastAdmin = Role.isAtLeast(user.globalRole, Role.ADMIN); + this.updateUserInformation(); + } + }); + } ngOnInit() { // Set currentLanguage to this.currentLanguage = Language.getByKey(localStorage.LANGUAGE) ?? Language.DEFAULT; - this.getUserInformation().then((userInformation) => { - this.form = { - formGroup: new FormGroup({}), - model: userInformation, - }; - - const baseInformationFields: FormlyFieldConfig[] = [{ - key: "street", - type: "input", - props: { - label: this.translate.instant("Register.Form.street"), - disabled: true, - }, - }, - { - key: "zip", - type: "input", - props: { - label: this.translate.instant("Register.Form.zip"), - disabled: true, - }, - }, - { - key: "city", - type: "input", - props: { - label: this.translate.instant("Register.Form.city"), - disabled: true, - }, - }, - { - key: "country", - type: "select", - props: { - label: this.translate.instant("Register.Form.country"), - options: COUNTRY_OPTIONS(this.translate), - disabled: true, - }, - }, - { - key: "email", - type: "input", - props: { - label: this.translate.instant("Register.Form.email"), - disabled: true, - }, - validators: { - validation: [Validators.email], - }, - }, - { - key: "phone", - type: "input", - props: { - label: this.translate.instant("Register.Form.phone"), - disabled: true, - }, - }]; - - if (Object.prototype.hasOwnProperty.call(userInformation, "companyName")) { - this.companyInformationFields.push( - { - key: "companyName", - type: "input", - props: { - label: this.translate.instant("Register.Form.companyName"), - disabled: true, - }, - }, - ...baseInformationFields, - ); - } else { - this.userInformationFields.push(...baseInformationFields); - } - }).then(() => { + this.updateUserInformation().then(() => { this.service.metadata.subscribe(entry => { this.showInformation = true; }); @@ -263,4 +201,82 @@ export class UserComponent implements OnInit { this.currentLanguage = language; this.translate.use(language.key); } + + private updateUserInformation(): Promise { + return this.getUserInformation().then((userInformation) => { + this.form = { + formGroup: new FormGroup({}), + model: userInformation, + }; + + const baseInformationFields: FormlyFieldConfig[] = [{ + key: "street", + type: "input", + props: { + label: this.translate.instant("Register.Form.street"), + disabled: true, + }, + }, + { + key: "zip", + type: "input", + props: { + label: this.translate.instant("Register.Form.zip"), + disabled: true, + }, + }, + { + key: "city", + type: "input", + props: { + label: this.translate.instant("Register.Form.city"), + disabled: true, + }, + }, + { + key: "country", + type: "select", + props: { + label: this.translate.instant("Register.Form.country"), + options: COUNTRY_OPTIONS(this.translate), + disabled: true, + }, + }, + { + key: "email", + type: "input", + props: { + label: this.translate.instant("Register.Form.email"), + disabled: true, + }, + validators: { + validation: [Validators.email], + }, + }, + { + key: "phone", + type: "input", + props: { + label: this.translate.instant("Register.Form.phone"), + disabled: true, + }, + + }]; + + if (Object.prototype.hasOwnProperty.call(userInformation, "companyName")) { + this.companyInformationFields = [{ + key: "companyName", + type: "input", + props: { + label: this.translate.instant("Register.Form.companyName"), + disabled: true, + }, + }, + ...baseInformationFields, + ]; + } else { + this.userInformationFields = baseInformationFields; + } + }); + } } diff --git a/ui/src/assets/i18n/cz.json b/ui/src/assets/i18n/cz.json index 04ac9dd63ec..7b90e504b9b 100644 --- a/ui/src/assets/i18n/cz.json +++ b/ui/src/assets/i18n/cz.json @@ -10,7 +10,6 @@ "changeAccepted": "ZmÄ›na byla pÅ™ijata", "changeFailed": "ZmÄ›na se nezdaÅ™ila", "chargeDischarge": "Debetní/vybíjení", - "chargePower": "Nabíjecí výkon", "componentCount": "PoÄet komponentů", "componentInactive": "Komponenta je neaktivní!", "connectionLost": "Spojení ztraceno. Pokouší se znovu pÅ™ipojit.", @@ -22,7 +21,6 @@ "digitalInputs": "Digitální vstupy", "numberOfComponents": "PoÄet komponentů", "directConsumption": "Přímá spotÅ™eba", - "dischargePower": "Vybíjecí výkon", "fault": "Chyba", "grid": "Síť", "gridBuy": "Nákup ze sítÄ›", @@ -96,7 +94,9 @@ "MINUTES": "Minuty", "DAY": "Den", "DAYS": "Dny" - } + }, + "DISCHARGE": "VypouÅ¡tÄ›ní", + "CHARGE": "NaÄítání" }, "Menu": { "accessLevel": "Úroveň přístupu", diff --git a/ui/src/assets/i18n/de.json b/ui/src/assets/i18n/de.json index 457f5cb15ab..2d1905b1c01 100644 --- a/ui/src/assets/i18n/de.json +++ b/ui/src/assets/i18n/de.json @@ -200,7 +200,9 @@ "name": "Erzwungene Beladung", "shortName": "Manuell" }, - "Uncontrollable": "Diese Ladesäule kann nicht gesteuert werden." + "Uncontrollable": "Diese Ladesäule kann nicht gesteuert werden.", + "HYSTERESIS": "Mindestumschaltzeit der Ladestation aktiv", + "HYSTERESIS_INFO": "Um Ladeabbrüche aufgrund zu häufigen Startens/Pausierens des Ladevorgangs zu verhindern, wird der aktuell eingestellte Lademodus für die nächsten Minuten fortgesetzt." }, "Heatingelement": { "activeForced": "Aktiv (Mindestlaufzeit)", @@ -235,14 +237,14 @@ "CONTROL_MODE_DESCRIPTION": { "CHARGE_CONSUMPTION": "" }, - "CHARGE_FROM_GRID_ACTIVATE": "Aktive Beladung aus dem Netz aktivieren (BETA-Test)", + "CHARGE_FROM_GRID_ACTIVATE": "Aktive Beladung aus dem Netz aktivieren", "PRICE": "Aktueller Bezugsstrompreis", "STATE": { "DELAY_DISCHARGE": "Entladung verzögert", "BALANCING": "Eigenverbrauchsoptimierung", "CHARGE_GRID": "Beladung aus dem Netz freigegeben" }, - "CHART_TITLE": "Aktueller Fahrplan (BETA-Test)", + "CHART_TITLE": "Aktueller Fahrplan", "CHART_WARNING_NOTE": "Die Grafik zeigt die vergangenen drei Stunden, sowie die zukünftig geplante Betriebsweise für den Zeitraum, für den die dynamischen Netzbezugspreise zur Verfügung stehen. Bitte beachten Sie, dass der Fahrplan kontinuierlich neu berechnet wird und sich somit im Tagesverlauf ändern kann.", "POWER_SOC_CHART_TITLE": "Vorhersagen (Nur für Admins)" }, @@ -271,6 +273,7 @@ "SYSTEMUPDATE": "Systemupdate" }, "History": { + "PHASE_ACCURATE": "Phasengenau", "CURRENT_AND_VOLTAGE": "Strom & Spannung", "beginDate": "Startdatum wählen", "day": "Tag", @@ -312,6 +315,7 @@ "nov": "Nov", "dec": "Dez", "activeDuration": "Einschaltdauer", + "ACTIVE_DURATION_WITH_LEVEL": "Einschaltdauer Level {{level}}", "CURRENT": "Strom", "VOLTAGE": "Spannung" }, @@ -467,7 +471,6 @@ "changeAccepted": "Änderung übernommen", "changeFailed": "Änderung fehlgeschlagen", "chargeDischarge": "Be-/Entladung", - "chargePower": "Beladung", "componentCount": "Anzahl Komponenten", "componentInactive": "Komponente ist inaktiv!", "connectionLost": "Verbindung unterbrochen. Versuche die Verbindung wiederherzustellen.", @@ -479,7 +482,6 @@ "digitalInputs": "Digitaleingänge", "numberOfComponents": "Anzahl der Komponenten", "directConsumption": "Direktverbrauch", - "dischargePower": "Entladung", "energyLimit": "Energielimit", "fault": "Fehler", "grid": "Netz", @@ -570,7 +572,9 @@ "MINUTES": "Minuten", "DAY": "Tag", "DAYS": "Tage" - } + }, + "CHARGE": "Beladung", + "DISCHARGE": "Entladung" }, "Index": { "allConnected": "Alle Verbindungen hergestellt.", diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index b3f33f87876..c6d1adbbb34 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -201,7 +201,9 @@ "name": "Force charging", "shortName": "Manually" }, - "Uncontrollable": "This charging station can not be controlled." + "Uncontrollable": "This charging station can not be controlled.", + "HYSTERESIS": "Minimum switching time of the charging station active", + "HYSTERESIS_INFO": "For preventing charging interruptions due to frequent starting/pausing of the charing process, the charging mode currently selected will continue for the next few minutes." }, "Heatingelement": { "activeForced": "Active (Minimum runtime)", @@ -236,14 +238,14 @@ "CONTROL_MODE_DESCRIPTION": { "CHARGE_CONSUMPTION": "" }, - "CHARGE_FROM_GRID_ACTIVATE": "Activate charge from the grid (BETA test)", + "CHARGE_FROM_GRID_ACTIVATE": "Activate charge from the grid", "PRICE": "Current price", "STATE": { "DELAY_DISCHARGE": "Delayed discharge", "BALANCING": "Self-Consumption optmization", "CHARGE_GRID": "Charge from grid allowed" }, - "CHART_TITLE": "Planned Schedule (BETA test)", + "CHART_TITLE": "Planned Schedule", "CHART_WARNING_NOTE": "The graphic shows the past three hours as well as the future planned operating mode for the period for which the dynamic grid purchase prices are available. Please note that the planned schedule is subject to continuous recalculation and may change throughout the day.", "POWER_SOC_CHART_TITLE": "Forecasts (Only for Admins)" }, @@ -311,10 +313,12 @@ "oct": "Oxt", "nov": "Nov", "dec": "Dec", - "activeDuration": "active duration", + "activeDuration": "Active duration", + "ACTIVE_DURATION_WITH_LEVEL": "Active duration level {{level}}", "CURRENT_AND_VOLTAGE": "Current & Voltage", "CURRENT": "Current", - "VOLTAGE": "Voltage" + "VOLTAGE": "Voltage", + "PHASE_ACCURATE": "Phasengenau" }, "Config": { "Index": { @@ -467,8 +471,7 @@ "capacity": "Capacity", "changeAccepted": "Change accepted", "changeFailed": "Change failed", - "chargeDischarge": "Charge/Discharge power", - "chargePower": "Charge power", + "chargeDischarge": "Charge/Discharge", "componentCount": "Number of components", "componentInactive": "Component is not active!", "connectionLost": "Connection lost. Trying to reconnect.", @@ -480,7 +483,6 @@ "digitalInputs": "Digital Inputs", "numberOfComponents": "Number of Components", "directConsumption": "Direct consumption", - "dischargePower": "Discharge power", "fault": "Fault", "FAILED": "failed", "WILL_BE_EXECUTED": "will be executed", @@ -572,7 +574,9 @@ "MINUTES": "Minutes", "DAY": "Day", "DAYS": "Days" - } + }, + "CHARGE": "Charge", + "DISCHARGE": "Discharge" }, "Index": { "allConnected": "All connections established.", diff --git a/ui/src/assets/i18n/es.json b/ui/src/assets/i18n/es.json index 251dd41029d..68f7cbe885b 100644 --- a/ui/src/assets/i18n/es.json +++ b/ui/src/assets/i18n/es.json @@ -9,7 +9,6 @@ "changeAccepted": "Cambio aceptado", "changeFailed": "Cambio fallido", "chargeDischarge": "Débito/Descarga", - "chargePower": "Carga", "componentCount": "Numero de componentes", "componentInactive": "El componente está inactivo!", "connectionLost": "Conexión perdida. Intentando reconectar.", @@ -20,7 +19,6 @@ "digitalInputs": "Entradas digitales", "numberOfComponents": "número de componentes", "directConsumption": "Consumo directo", - "dischargePower": "Descarga", "fault": "Error", "grid": "Red", "gridBuy": "Relación", @@ -91,7 +89,9 @@ "MINUTES": "Minutos", "DAY": "Día", "DAYS": "Días" - } + }, + "DISCHARGE": "Descargar", + "CHARGE": "Cargando" }, "Menu": { "accessLevel": "Nivel de acceso", diff --git a/ui/src/assets/i18n/fr.json b/ui/src/assets/i18n/fr.json index 2ead4d8e8c0..2d3c86ae82d 100644 --- a/ui/src/assets/i18n/fr.json +++ b/ui/src/assets/i18n/fr.json @@ -11,7 +11,6 @@ "changeAccepted": "Changement accepté", "changeFailed": "Changement échoué", "chargeDischarge": "Puissance de Charge/Décharge", - "chargePower": "Puissance de charge", "componentInactive": "Component is not active!", "connectionLost": "Connection lost. Trying to reconnect.", "consumption": "Consommation", @@ -19,7 +18,6 @@ "currentName": "current name", "currentValue": "current value", "dateFormat": "yyyy-MM-dd", - "dischargePower": "Puissance de décharge", "digitalInputs": "Entrées numériques", "fault": "Fault", "grid": "Réseau", @@ -93,7 +91,9 @@ "MINUTES": "Minutes", "DAY": "Journée", "DAYS": "Journées" - } + }, + "DISCHARGE": "Décharge", + "CHARGE": "Charge" }, "Menu": { "accessLevel": "Niveau d'accès", diff --git a/ui/src/assets/i18n/ja.json b/ui/src/assets/i18n/ja.json index 06a7294eb4f..5ec3a322dd5 100644 --- a/ui/src/assets/i18n/ja.json +++ b/ui/src/assets/i18n/ja.json @@ -410,7 +410,6 @@ "changeAccepted": "変更ãŒæ‰¿èªã•ã‚Œã¾ã—ãŸ", "changeFailed": "変更ãŒå¤±æ•—ã—ã¾ã—ãŸ", "chargeDischarge": "充電・放電パワー", - "chargePower": "å……é›»", "componentCount": "部å“æ•°", "componentInactive": "無効化中", "connectionLost": "接続ãŒåˆ‡æ–­ã•ã‚Œã¾ã—ãŸã€‚å†æŽ¥ç¶šã‚’試ã¿ã¾ã™ã€‚", @@ -422,7 +421,6 @@ "digitalInputs": "デジタル入力", "numberOfComponents": "部å“æ•°", "directConsumption": "直接消費", - "dischargePower": "放電", "fault": "障害", "grid": "グリッド", "gridBuy": "è²·é›»", @@ -503,7 +501,9 @@ "yes": "ã¯ã„", "no": "ã„ã„ãˆ", "value": "値", - "SUM_STATE": "システム状態" + "SUM_STATE": "システム状態", + "DISCHARGE": "放電", + "CHARGE": "å……é›»" }, "Index": { "allConnected": "ã™ã¹ã¦ã®æŽ¥ç¶šãŒç¢ºç«‹ã•ã‚Œã¦ã„ã¾ã™ã€‚", diff --git a/ui/src/assets/i18n/nl.json b/ui/src/assets/i18n/nl.json index 77d013c0482..8104de8fd3d 100644 --- a/ui/src/assets/i18n/nl.json +++ b/ui/src/assets/i18n/nl.json @@ -9,7 +9,6 @@ "changeAccepted": "Wijziging geaccepteerd", "changeFailed": "Wijziging mislukt", "chargeDischarge": "Debet/ontlaad", - "chargePower": "Laad vermogen", "componentCount": "Aantal componenten", "componentInactive": "Component is inactief!", "connectionLost": "Verbinding verbroken. Probeer opnieuw verbinding te maken.", @@ -20,7 +19,6 @@ "digitalInputs": "Digitale Ingangen", "numberOfComponents": "aantal componenten", "directConsumption": "Directe consumptie", - "dischargePower": "Ontlaad vermogen", "fault": "Fout", "grid": "Net", "gridBuy": "Netafname", @@ -88,7 +86,9 @@ "MINUTES": "Minuten", "DAY": "Dag", "DAYS": "Dagen" - } + }, + "DISCHARGE": "Afvoer", + "CHARGE": "Laden" }, "Menu": { "accessLevel": "Toegangsniveau", diff --git a/ui/src/global-ion-custom.scss b/ui/src/global-ion-custom.scss new file mode 100644 index 00000000000..95979a5ee40 --- /dev/null +++ b/ui/src/global-ion-custom.scss @@ -0,0 +1,11 @@ +.ion-font-size-medium { + font-size: medium !important; +} + +.ion-font-size-smaller { + font-size: small !important; +} + +.ion-font-weight-bolder { + font-weight: bolder; +} diff --git a/ui/src/global.scss b/ui/src/global.scss index 59a24c31264..64cf303577b 100644 --- a/ui/src/global.scss +++ b/ui/src/global.scss @@ -25,6 +25,7 @@ /* ngx-spinner */ @import "node_modules/ngx-spinner/animations/ball-clip-rotate-multiple.css"; @import "variables"; +@import "./global-ion-custom.scss"; /* Live- and HistoryComponent*/ ion-refresher-content { @@ -54,6 +55,13 @@ ion-refresher-content { } +.disabled { + color: gray; + pointer-events: none; + opacity: 0.5; + /* Makes it semi-transparent */ +} + formly-wrapper-ion-form-field, formly-input-serial-number, formly-field-ion-radio { @@ -403,4 +411,22 @@ ion-modal.full-width { --storage-segment-2: block; --storage-segment-3: block; --storage-segment-4: block; -} \ No newline at end of file +} + +.card-with-primary-border { + border: 2px solid $primary-color; +} + +ion-avatar.iconify { + margin-inline-end: 32px; + margin-top: 12px; + margin-bottom: 12px; + --border-radius: 0; + max-height: 24px; + max-width: 24px; +} + +ion-avatar.iconify.icon-large { + max-height: 32px; + max-width: 32px; +}
    General.chargePowerGeneral.CHARGE {{ (sum.effectivePower <= 0 ? (sum.effectivePower * -1) : null) | unitvalue:'W' }}
    General.dischargePowerGeneral.DISCHARGE {{ (sum.effectivePower > 0 ? sum.effectivePower : null) | unitvalue:'W' }}
    General.phase {{ phase }} General.dischargePower + translate>General.DISCHARGE {{ value | unitvalue:'W' }} @@ -61,7 +61,7 @@
    General.phase {{ phase }} General.chargePower + translate>General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -76,7 +76,7 @@
    General.phase {{ phase }} General.dischargePower + translate>General.DISCHARGE {{ value | unitvalue:'W' }} @@ -85,7 +85,7 @@
    General.phase {{ phase }} General.chargePower + translate>General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -316,13 +316,13 @@
    General.chargePowerGeneral.CHARGE {{ sum.effectiveChargePower | unitvalue:'W' }}
    General.dischargePowerGeneral.DISCHARGE {{ sum.effectiveDischargePower | unitvalue:'W' }}
    General.phase {{ phase }} - General.dischargePower + General.DISCHARGE {{ value | unitvalue:'W' }} @@ -349,7 +349,7 @@
    General.phase {{ phase }} - General.chargePower + General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -364,7 +364,7 @@
    General.phase {{ phase }} - General.dischargePower + General.DISCHARGE {{ value | unitvalue:'W' }} @@ -373,7 +373,7 @@
    General.phase {{ phase }} - General.chargePower + General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -437,12 +437,12 @@ *ngIf="factory.natureIds.includes('io.openems.edge.ess.api.SymmetricEss') && value !== null && value !== undefined" class="full_width">
    General.chargePowerGeneral.CHARGE {{ (value <= 0 ? (value * -1) : null) | unitvalue:'W' }}
    General.dischargePowerGeneral.DISCHARGE {{ (value > 0 ? value : null) | unitvalue:'W' }} @@ -458,7 +458,7 @@ General.phase {{ phase }} - General.dischargePower + General.DISCHARGE {{ value | unitvalue:'W' }} @@ -467,7 +467,7 @@ General.phase {{ phase }} - General.chargePower + General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -696,14 +696,14 @@ {{ component.alias }}
    General.chargePowerGeneral.CHARGE {{ (value > 0 ? value : null) | unitvalue:'W' }}
    General.dischargePowerGeneral.DISCHARGE {{ (value <= 0 ? (value * -1) : null | unitvalue:'W') }} + -   {{ displayValue }} + {{displayName ? displayName + ": " + displayValue: '  ' + + displayValue}}
    - {{ name }} + {{ displayName }} + {{ displayValue }}