From c8418e1029f35add9771dcef415ab563feb4b785 Mon Sep 17 00:00:00 2001 From: Franz Reiter Date: Thu, 18 Jun 2026 12:59:21 +0200 Subject: [PATCH] [Edge] Implemented EEBUS bridge & power limitation controllers for eebus - Added EEBUS bridge - Added Controller.Ess.RippleControlReceiver.Eebus component - Added Controller.Ess.Limiter14a.Eebus component - Does not build currently because there is no maven repository for latest jeebus code --- cnf/pom.xml | 47 +++ io.openems.common.bridge.eebus/.classpath | 12 + io.openems.common.bridge.eebus/.gitignore | 2 + io.openems.common.bridge.eebus/.project | 23 ++ .../org.eclipse.core.resources.prefs | 2 + io.openems.common.bridge.eebus/bnd.bnd | 13 + io.openems.common.bridge.eebus/readme.adoc | 6 + .../eebus/api/EebusDeviceDiscovery.java | 54 ++++ .../common/bridge/eebus/api/package-info.java | 3 + .../common/bridge/eebus/package-info.java | 3 + .../test/.gitignore | 0 io.openems.edge.application/EdgeApp.bndrun | 28 +- io.openems.edge.bridge.eebus/.classpath | 12 + io.openems.edge.bridge.eebus/.gitignore | 3 + io.openems.edge.bridge.eebus/.project | 23 ++ .../org.eclipse.core.resources.prefs | 2 + io.openems.edge.bridge.eebus/bnd.bnd | 27 ++ io.openems.edge.bridge.eebus/readme.adoc | 88 ++++++ .../edge/bridge/eebus/BridgeEebusImpl.java | 279 ++++++++++++++++++ .../io/openems/edge/bridge/eebus/Config.java | 37 +++ .../eebus/ConfigCertificateStorage.java | 179 +++++++++++ .../edge/bridge/eebus/DelayedUpdater.java | 65 ++++ .../edge/bridge/eebus/api/BridgeEebus.java | 57 ++++ .../edge/bridge/eebus/api/EebusPeer.java | 69 +++++ .../bridge/eebus/api/EebusUseCaseManager.java | 13 + .../edge/bridge/eebus/api/package-info.java | 3 + .../edge/bridge/eebus/peer/EebusPeerImpl.java | 77 +++++ .../bridge/eebus/peer/InternalEebusPeer.java | 9 + .../edge/bridge/eebus/peer/PeerConfig.java | 26 ++ .../edge/bridge/eebus/peer/package-info.java | 3 + .../bridge/eebus/test/DummyBridgeEebus.java | 64 ++++ .../bridge/eebus/test/DummyEebusPeer.java | 31 ++ .../eebus/test/DummyLimitPowerProduction.java | 9 + .../bridge/eebus/test/DummyLimitation.java | 12 + .../edge/bridge/eebus/test/package-info.java | 3 + .../bridge/eebus/usecase/EebusUseCase.java | 11 + .../usecase/EebusUseCaseManagerImpl.java | 99 +++++++ .../eebus/usecase/EebusUseCaseType.java | 30 ++ .../LimitPowerConsumptionUseCase.java | 101 +++++++ .../LimitPowerProductionUseCase.java | 101 +++++++ .../api/ILimitPowerConsumptionHandler.java | 7 + .../api/ILimitPowerProductionHandler.java | 7 + .../powerlimitation/api/LimitPowerState.java | 38 +++ .../powerlimitation/api/package-info.java | 3 + io.openems.edge.bridge.eebus/test/.gitignore | 0 .../edge/bridge/eebus/BridgeEebusTest.java | 11 + .../eebus/ConfigCertificateStorageTest.java | 36 +++ .../src/io/openems/edge/common/sum/Sum.java | 13 + .../ControllerEssGridOptimizedChargeImpl.java | 11 +- ...trollerEssGridOptimizedChargeImplTest.java | 4 +- .../bnd.bnd | 1 + .../ess/limiter14a/eebus/Config.java | 29 ++ .../eebus/ControllerEssLimiter14aEebus.java | 65 ++++ .../ControllerEssLimiter14aEebusImpl.java | 165 +++++++++++ .../ess/limiter14a/eebus/package-info.java | 3 + .../limiter14a/{ => relaissignal}/Config.java | 10 +- .../ControllerEssLimiter14aRelaisSignal.java | 6 + ...trollerEssLimiter14aRelaisSignalImpl.java} | 51 ++-- .../limiter14a/relaissignal/package-info.java | 3 + .../controller/ess/limiter14a/MyConfig.java | 1 + .../bnd.bnd | 1 + .../ControllerEssRippleControlReceiver.java | 89 ------ .../PowerProductionLimiter.java | 12 + .../PowerProductionLimiterComponent.java | 37 +++ .../ripplecontrolreceiver/eebus/Config.java | 25 ++ .../eebus/EebusPowerProductionLimiter.java | 65 ++++ .../EebusPowerProductionLimiterImpl.java | 162 ++++++++++ .../{ => powerproduction}/Config.java | 6 +- .../RelaisSignalPowerProductionLimiter.java | 41 +++ ...laisSignalPowerProductionLimiterImpl.java} | 165 ++++++----- .../test/DummyPowerProductionLimiter.java | 55 ++++ .../test/DummyRippleControlReceiver.java | 50 ---- .../ess/ripplecontrolreceiver/MyConfig.java | 1 + io.openems.edge.core/bnd.bnd | 1 + .../PredictorManagerImpl.java | 2 +- .../GoodWeBatteryInverterImpl.java | 10 +- 76 files changed, 2567 insertions(+), 245 deletions(-) create mode 100644 io.openems.common.bridge.eebus/.classpath create mode 100644 io.openems.common.bridge.eebus/.gitignore create mode 100644 io.openems.common.bridge.eebus/.project create mode 100644 io.openems.common.bridge.eebus/.settings/org.eclipse.core.resources.prefs create mode 100644 io.openems.common.bridge.eebus/bnd.bnd create mode 100644 io.openems.common.bridge.eebus/readme.adoc create mode 100644 io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/api/EebusDeviceDiscovery.java create mode 100644 io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/api/package-info.java create mode 100644 io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/package-info.java create mode 100644 io.openems.common.bridge.eebus/test/.gitignore create mode 100644 io.openems.edge.bridge.eebus/.classpath create mode 100644 io.openems.edge.bridge.eebus/.gitignore create mode 100644 io.openems.edge.bridge.eebus/.project create mode 100644 io.openems.edge.bridge.eebus/.settings/org.eclipse.core.resources.prefs create mode 100644 io.openems.edge.bridge.eebus/bnd.bnd create mode 100644 io.openems.edge.bridge.eebus/readme.adoc create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/BridgeEebusImpl.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/Config.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/ConfigCertificateStorage.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/DelayedUpdater.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/BridgeEebus.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/EebusPeer.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/EebusUseCaseManager.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/package-info.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/EebusPeerImpl.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/InternalEebusPeer.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/PeerConfig.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/package-info.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyBridgeEebus.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyEebusPeer.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyLimitPowerProduction.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyLimitation.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/package-info.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCase.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCaseManagerImpl.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCaseType.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/LimitPowerConsumptionUseCase.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/LimitPowerProductionUseCase.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/ILimitPowerConsumptionHandler.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/ILimitPowerProductionHandler.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/LimitPowerState.java create mode 100644 io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/package-info.java create mode 100644 io.openems.edge.bridge.eebus/test/.gitignore create mode 100644 io.openems.edge.bridge.eebus/test/io/openems/edge/bridge/eebus/BridgeEebusTest.java create mode 100644 io.openems.edge.bridge.eebus/test/io/openems/edge/bridge/eebus/ConfigCertificateStorageTest.java create mode 100644 io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/Config.java create mode 100644 io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/ControllerEssLimiter14aEebus.java create mode 100644 io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/ControllerEssLimiter14aEebusImpl.java create mode 100644 io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/package-info.java rename io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/{ => relaissignal}/Config.java (80%) create mode 100644 io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/ControllerEssLimiter14aRelaisSignal.java rename io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/{ControllerEssLimiter14aImpl.java => relaissignal/ControllerEssLimiter14aRelaisSignalImpl.java} (65%) create mode 100644 io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/package-info.java delete mode 100644 io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/ControllerEssRippleControlReceiver.java create mode 100644 io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/PowerProductionLimiter.java create mode 100644 io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/PowerProductionLimiterComponent.java create mode 100644 io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/Config.java create mode 100644 io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/EebusPowerProductionLimiter.java create mode 100644 io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/EebusPowerProductionLimiterImpl.java rename io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/{ => powerproduction}/Config.java (88%) create mode 100644 io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/RelaisSignalPowerProductionLimiter.java rename io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/{ControllerEssRippleControlReceiverImpl.java => powerproduction/RelaisSignalPowerProductionLimiterImpl.java} (51%) create mode 100644 io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/test/DummyPowerProductionLimiter.java delete mode 100644 io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/test/DummyRippleControlReceiver.java diff --git a/cnf/pom.xml b/cnf/pom.xml index d9aaa01a403..32a278837c8 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -56,6 +56,33 @@ jackson-annotations 2.21 + + + com.fasterxml.jackson.module + jackson-module-jakarta-xmlbind-annotations + 2.21.2 + + + jakarta.xml.bind + jakarta.xml.bind-api + 4.0.5 + + + org.jvnet.jaxb + jaxb-plugins-runtime + 4.0.12 + + + org.glassfish.jaxb + jaxb-runtime + 4.0.7 + + + com.sun.xml.bind + jaxb-impl + 4.0.7 + + com.github.rzymek opczip @@ -866,5 +893,25 @@ objenesis 3.5 + + io.netty + netty-transport + 4.2.12.Final + + + io.netty + netty-handler + 4.2.12.Final + + + io.netty + netty-common + 4.2.12.Final + + + io.netty + netty-codec-http + 4.2.12.Final + \ No newline at end of file diff --git a/io.openems.common.bridge.eebus/.classpath b/io.openems.common.bridge.eebus/.classpath new file mode 100644 index 00000000000..b4cffd0fe60 --- /dev/null +++ b/io.openems.common.bridge.eebus/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.common.bridge.eebus/.gitignore b/io.openems.common.bridge.eebus/.gitignore new file mode 100644 index 00000000000..c2b941a96de --- /dev/null +++ b/io.openems.common.bridge.eebus/.gitignore @@ -0,0 +1,2 @@ +/bin_test/ +/generated/ diff --git a/io.openems.common.bridge.eebus/.project b/io.openems.common.bridge.eebus/.project new file mode 100644 index 00000000000..a251b6a01bd --- /dev/null +++ b/io.openems.common.bridge.eebus/.project @@ -0,0 +1,23 @@ + + + io.openems.common.bridge.eebus + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.common.bridge.eebus/.settings/org.eclipse.core.resources.prefs b/io.openems.common.bridge.eebus/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/io.openems.common.bridge.eebus/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/io.openems.common.bridge.eebus/bnd.bnd b/io.openems.common.bridge.eebus/bnd.bnd new file mode 100644 index 00000000000..fe359dc1daf --- /dev/null +++ b/io.openems.common.bridge.eebus/bnd.bnd @@ -0,0 +1,13 @@ +Bundle-Name: OpenEMS Common EEBUS Bridge +Bundle-Vendor: FENECON GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 1.0.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + io.openems.common,\ + javax.jmdns,\ + org.openmuc.jeebus.ship,\ + +-testpath: \ + ${testpath} diff --git a/io.openems.common.bridge.eebus/readme.adoc b/io.openems.common.bridge.eebus/readme.adoc new file mode 100644 index 00000000000..d9c04670897 --- /dev/null +++ b/io.openems.common.bridge.eebus/readme.adoc @@ -0,0 +1,6 @@ += Http + +Http is a widely used standard for web communication. It is used for some hardware devices like electric meters, relays and so on. +The standard architecture used by the devices are RESTful api's. + +https://github.com/OpenEMS/openems/tree/develop/io.openems.common.http[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/api/EebusDeviceDiscovery.java b/io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/api/EebusDeviceDiscovery.java new file mode 100644 index 00000000000..313e6c6ab61 --- /dev/null +++ b/io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/api/EebusDeviceDiscovery.java @@ -0,0 +1,54 @@ +package io.openems.common.bridge.eebus.api; + +import org.openmuc.jeebus.ship.api.ConnectionHandler; +import org.openmuc.jeebus.ship.api.DisconnectReason; +import org.openmuc.jeebus.ship.api.ShipConnectionInterface; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.Objects; + +@Component(scope = ServiceScope.SINGLETON, service = EebusDeviceDiscovery.class) +public class EebusDeviceDiscovery { + /*private ServiceRegistry registry; + + @Activate + public void activate() { + this.registry = new ServiceRegistry(Inet4Address.getLoopbackAddress(), "me", "_ship._tcp.local.", new ConnectionHandler() { + @Override + public void onMessageReceived(byte[] fullMsg, byte[] payload, ShipConnectionInterface shipConn) { + } + + @Override + public void onDisconnect(DisconnectReason reason, ShipConnectionInterface shipConn) { + } + + @Override + public void serviceAdded(String socketAddress, String ski) { + System.out.println("Service resolved: " + socketAddress + " - " + ski); + // TODO@fenecon: this is called whenever a SHIP mDNS Service is + // resolved. + // If the socket and SKI are not enough, you may use + // registry.listServices(); and filter that for the full + // service info. It is an ugly work-around though. + System.out.println(Arrays.stream(EebusDeviceDiscovery.this.registry.listServices()) + .filter(s -> Objects.equals( + s.getAddress().getHostAddress() + ":" + s.getPort(), + socketAddress + )).findAny().get()); + } + + @Override + public void serviceRemoved(String ipAddr) { + } + + @Override + public void connectionDataExchangeEnabled(String ipAddr) { + } + }); + }*/ + +} diff --git a/io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/api/package-info.java b/io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/api/package-info.java new file mode 100644 index 00000000000..a7b0e06cb18 --- /dev/null +++ b/io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/api/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.common.bridge.eebus.api; diff --git a/io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/package-info.java b/io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/package-info.java new file mode 100644 index 00000000000..3fd2c532183 --- /dev/null +++ b/io.openems.common.bridge.eebus/src/io/openems/common/bridge/eebus/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.common.bridge.eebus; diff --git a/io.openems.common.bridge.eebus/test/.gitignore b/io.openems.common.bridge.eebus/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 5f7dcab0e42..92b2e2fcf79 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -58,6 +58,7 @@ bnd.identity;id='io.openems.edge.bridge.modbus',\ bnd.identity;id='io.openems.edge.bridge.mqtt',\ bnd.identity;id='io.openems.edge.bridge.onewire',\ + bnd.identity;id='io.openems.edge.bridge.eebus',\ bnd.identity;id='io.openems.edge.common',\ bnd.identity;id='io.openems.edge.controller.api.backend',\ bnd.identity;id='io.openems.edge.controller.api.common',\ @@ -229,6 +230,7 @@ bcprov;version='[1.84.0,1.84.1)',\ bcutil;version='[1.84.0,1.84.1)',\ com.fasterxml.aalto-xml;version='[1.4.0,1.4.1)',\ + com.fasterxml.jackson.core.jackson-annotations;version='[2.21.0,2.21.1)',\ com.fazecast.jSerialComm;version='[2.11.4,2.11.5)',\ com.google.gson;version='[2.14.0,2.14.1)',\ com.google.guava;version='[33.6.0,33.6.1)',\ @@ -261,6 +263,7 @@ io.openems.edge.bridge.modbus;version=snapshot,\ io.openems.edge.bridge.mqtt;version=snapshot,\ io.openems.edge.bridge.onewire;version=snapshot,\ + io.openems.edge.bridge.eebus;version=snapshot,\ io.openems.edge.common;version=snapshot,\ io.openems.edge.controller.api;version=snapshot,\ io.openems.edge.controller.api.backend;version=snapshot,\ @@ -503,4 +506,27 @@ org.owasp.encoder;version='[1.4.0,1.4.1)',\ reactive-streams;version='[1.0.4,1.0.5)',\ rrd4j;version='[3.10.0,3.10.1)',\ - stax2-api;version='[4.3.0,4.3.1)' \ No newline at end of file + stax2-api;version='[4.3.0,4.3.1)',\ + io.netty.buffer;version='[4.2.12,4.2.13)',\ + io.netty.codec-base;version='[4.2.12,4.2.13)',\ + io.netty.codec-compression;version='[4.2.12,4.2.13)',\ + io.netty.codec-http;version='[4.2.12,4.2.13)',\ + io.netty.common;version='[4.2.12,4.2.13)',\ + io.netty.handler;version='[4.2.12,4.2.13)',\ + io.netty.resolver;version='[4.2.12,4.2.13)',\ + io.netty.transport;version='[4.2.12,4.2.13)',\ + io.netty.transport-native-unix-common;version='[4.2.12,4.2.13)',\ + org.openmuc.jeebus.ship;version='[2.2.1,2.2.2)',\ + io.openems.common.bridge.eebus;version=snapshot,\ + com.fasterxml.jackson.core.jackson-core;version='[2.21.2,2.21.3)',\ + com.fasterxml.jackson.core.jackson-databind;version='[2.21.2,2.21.3)',\ + com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations;version='[2.21.2,2.21.3)',\ + com.sun.xml.bind.jaxb-core;version='[4.0.7,4.0.8)',\ + jakarta.activation-api;version='[2.1.4,2.1.5)',\ + jakarta.xml.bind-api;version='[4.0.5,4.0.6)',\ + org.glassfish.jaxb.core;version='[4.0.7,4.0.8)',\ + org.jvnet.jaxb.plugins-runtime;version='[4.0.12,4.0.13)',\ + org.openmuc.jeebus.spine;version='[4.1.0,4.1.1)',\ + org.openmuc.jeebus.usecase.powerlimitation.abstract-controllablesystem;version='[1.0.0,1.0.1)',\ + org.openmuc.jeebus.usecase.powerlimitation.lpc-controllablesystem;version='[1.0.0,1.0.1)',\ + org.openmuc.jeebus.usecase.powerlimitation.lpp-controllablesystem;version='[1.0.0,1.0.1)',\ \ No newline at end of file diff --git a/io.openems.edge.bridge.eebus/.classpath b/io.openems.edge.bridge.eebus/.classpath new file mode 100644 index 00000000000..b4cffd0fe60 --- /dev/null +++ b/io.openems.edge.bridge.eebus/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.bridge.eebus/.gitignore b/io.openems.edge.bridge.eebus/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/io.openems.edge.bridge.eebus/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/io.openems.edge.bridge.eebus/.project b/io.openems.edge.bridge.eebus/.project new file mode 100644 index 00000000000..be03e86172c --- /dev/null +++ b/io.openems.edge.bridge.eebus/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.bridge.eebus + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.bridge.eebus/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.bridge.eebus/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/io.openems.edge.bridge.eebus/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/io.openems.edge.bridge.eebus/bnd.bnd b/io.openems.edge.bridge.eebus/bnd.bnd new file mode 100644 index 00000000000..93fe8513918 --- /dev/null +++ b/io.openems.edge.bridge.eebus/bnd.bnd @@ -0,0 +1,27 @@ +Bundle-Name: OpenEMS Edge Bridge EEBUS +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.common.bridge.eebus,\ + io.openems.common.bridge.http,\ + io.openems.edge.common,\ + org.openmuc.jeebus.usecase.powerlimitation.abstract-controllablesystem,\ + org.openmuc.jeebus.ship,\ + org.openmuc.jeebus.spine,\ + org.openmuc.jeebus.usecase.powerlimitation.lpc-controllablesystem,\ + org.openmuc.jeebus.usecase.powerlimitation.lpp-controllablesystem,\ + jakarta.xml.bind-api;version='[4.0.5,4.0.6)',\ + org.jvnet.jaxb.plugins-runtime;version='[4.0.12,4.0.13)',\ + bcpkix;version='[1.83,2)',\ + bcprov;version='[1.83,2)',\ + bcutil;version='[1.83,2)',\ + +-testpath: \ + ${testpath},\ + bcpkix;version='[1.83,2)',\ + bcprov;version='[1.83,2)',\ + bcutil;version='[1.83,2)',\ diff --git a/io.openems.edge.bridge.eebus/readme.adoc b/io.openems.edge.bridge.eebus/readme.adoc new file mode 100644 index 00000000000..3718ed25e30 --- /dev/null +++ b/io.openems.edge.bridge.eebus/readme.adoc @@ -0,0 +1,88 @@ += OneWire + +OneWire is a widely used protocol for home automation. It is used by sensors like thermometers. + +== Setup + +This implementation directly talks to the OneWire busmaster, e.g. a DS9490R USB dongle, without requiring the OneWire File System (OWFS). +It therefore requires the natively compiled C library by the manufacturer of the OneWire chip Maxim Integrated. +This library is then accessed using Java JNI. + +To compile the native library follow the guide on https://www.maximintegrated.com/en/design/technical-documents/app-notes/5/5917.html. + +. Download the OneWireViewer-Linux.zip file +. Unzip the file (`unzip OneWireViewer-Linux.zip`) +. Prepare compile environment (`sudo apt-get install libusb-dev build-essential`) +. Install Java JDK (e.g. `sudo apt-get install openjdk-8-jdk`) +. Compile (`cd OneWireViewer-Linux/PDKAdapterUSB && make && sudo make install`) + +The library needs exclusive access to the busmaster. + +. Unload the default Linux driver: `rmmod ds2490` +. Block the driver from loading on next boot `echo blacklist ds2490 > /etc/modprobe.d/ds2490.conf` + +== Identifying devices + +The OneWire Bridge provides a JSON-RPC Request that reads all devices connected to the OneWire bus. + +Example: read via OpenEMS Backend + +[source,json] +---- +{ + "method": "edgeRpc", + "params": { + "edgeId": "edge0", + "payload":{ + "method": "componentJsonApi", + "params": { + "componentId": "onewire0", + "payload": { + "method": "getDevices", + "params": {} + } + } + } + } +} +---- + +Example Response: + +[source,json] +---- +{ + "jsonrpc":"2.0", + "id":"UUID", + "result":{ + "payload":{ + "jsonrpc":"2.0", + "id":"UUID", + "result":{ + "devices":[ + { + "address":"4D0000094xxxxxxx", + "name":"DS18B20", + "alternateName":"DS1820B, DS18B20X", + "description":"Digital thermometer measures temperatures from -55C to 125C in 0.75 seconds (max). +/- 0.5C accuracy between -10C and 85C. Thermometer resolution is programmable at 9, 10, 11, and 12 bits. ", + "details":{ + "type":"TemperatureContainer", + "temperature":17.5625 + } + }, + { + "address":"6F0000022xxxxxxx", + "name":"DS2438", + "alternateName":"Smart Battery Monitor", + "description":"1-Wire device that integrates the total current charging or discharging through a battery and stores it in a register. It also returns the temperature (accurate to 2 degrees celsius), as well as the instantaneous current and voltage and also provides 40 bytes of EEPROM storage.", + "details":{ + "type":"TemperatureContainer", + "temperature":19.3125 + } + } + ] + } + } + } +} +---- \ No newline at end of file diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/BridgeEebusImpl.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/BridgeEebusImpl.java new file mode 100644 index 00000000000..081a019d873 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/BridgeEebusImpl.java @@ -0,0 +1,279 @@ +package io.openems.edge.bridge.eebus; + +import static io.openems.edge.common.channel.ChannelUtils.setValue; +import static org.openmuc.jeebus.shipspine.ShipCommunication.ConnectClientsTo.TRUSTED; +import static org.osgi.service.component.annotations.ConfigurationPolicy.REQUIRE; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +import io.openems.common.bridge.http.time.DelayTimeProvider; +import io.openems.common.bridge.http.time.DelayTimeProviderChain; +import io.openems.common.bridge.http.time.periodic.PeriodicExecutor; +import io.openems.common.bridge.http.time.periodic.PeriodicExecutorFactory; +import org.openmuc.jeebus.ship.api.ShipConnectionInfoSnapshot; +import org.openmuc.jeebus.ship.api.ShipNodeConfiguration; +import org.openmuc.jeebus.ship.node.KeyManagement; +import org.openmuc.jeebus.shipspine.ShipCommunication; +import org.openmuc.jeebus.spine.api.Device; +import org.openmuc.jeebus.spine.xsd.v1.DeviceTypeEnumType; +import org.openmuc.jeebus.spine.xsd.v1.EntityTypeEnumType; +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.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.event.Event; +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 com.google.common.collect.ImmutableList; + +import io.openems.common.bridge.eebus.api.EebusDeviceDiscovery; +import io.openems.common.referencetarget.GenerateTargetsFromReferences; +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.bridge.eebus.api.EebusPeer; +import io.openems.edge.bridge.eebus.api.EebusUseCaseManager; +import io.openems.edge.bridge.eebus.peer.InternalEebusPeer; +import io.openems.edge.bridge.eebus.usecase.EebusUseCaseManagerImpl; +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.meta.Meta; + +@Designate(ocd = Config.class, factory = true) +@Component(name = "Bridge.EEBUS", // + immediate = true, // + configurationPolicy = REQUIRE // +) +@EventTopics({ // + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, // +}) +@GenerateTargetsFromReferences("Peer") +public class BridgeEebusImpl extends AbstractOpenemsComponent implements BridgeEebus, OpenemsComponent, EventHandler { + @Reference + protected EebusDeviceDiscovery discovery; + + @Reference + private ConfigurationAdmin cm; + + @Reference + private Meta meta; + + @Reference + private PeriodicExecutorFactory periodicExecutorFactory; + + private final Logger log = LoggerFactory.getLogger(BridgeEebusImpl.class); + private final EebusUseCaseManagerImpl useCaseManager; + private final List peers = new CopyOnWriteArrayList<>(); + + private ConfigCertificateStorage certificateStorage; + private ShipNodeConfiguration shipNodeConfig; + private ShipCommunication shipCommunication; + private Device eebusDevice; + private int connectionsAmount = 0; + + private PeriodicExecutor reInitScheduler; + private boolean reInitRequired = true; + + public BridgeEebusImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + BridgeEebus.ChannelId.values() // + ); + + this.useCaseManager = new EebusUseCaseManagerImpl(this); + } + + @Activate + protected void activate(ComponentContext context, Config config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + + this.certificateStorage = new ConfigCertificateStorage(this.cm, this.servicePid()); + this.shipNodeConfig = this.createNodeConfig(config); + + this.reInitScheduler = this.periodicExecutorFactory.execute("BridgeEebus-DeviceReInit", () -> { + if (this.reInitRequired || this.useCaseManager.isDirty()) { + this.reinitializeDevice(); + } + return DelayTimeProviderChain.fixedDelay(Duration.ofMinutes(1)).getDelay(); + }, DelayTimeProvider.Delay.of(Duration.ofSeconds(5))); + } + + @Modified + protected void modified(ComponentContext context, Config config) { + super.modified(context, config.id(), config.alias(), config.enabled()); + + this.certificateStorage = new ConfigCertificateStorage(this.cm, this.servicePid()); + this.shipNodeConfig = this.createNodeConfig(config); + this.reInitRequired = true; + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + this.unloadDevice(); + + if (this.reInitScheduler != null) { + this.reInitScheduler.dispose(); + this.reInitScheduler = null; + } + } + + @Reference(// + target = "(&(eebus.id=${config.id})(enabled=true))", // + bind = "bindPeer", // + unbind = "unbindPeer", // + policyOption = ReferencePolicyOption.GREEDY, // + policy = ReferencePolicy.DYNAMIC, // + cardinality = ReferenceCardinality.MULTIPLE // + ) + protected synchronized void bindPeer(InternalEebusPeer peer) { + this.peers.add(peer); + this.reInitRequired = true; + } + + protected synchronized void unbindPeer(InternalEebusPeer peer) { + this.peers.remove(peer); + this.reInitRequired = true; + } + + protected void reinitializeDevice() { + this.reInitRequired = false; + + this.log.info("Initializing eebus device ..."); + this.unloadDevice(); + this.initDevice(); + this.log.info("Initialized eebus device."); + } + + protected void unloadDevice() { + if (this.eebusDevice != null) { + this.eebusDevice.close(); + this.eebusDevice = null; + } + } + + protected void initDevice() { + if (!this.isEnabled()) { + return; + } + + try { + var shipCommunication = new ShipCommunication(this.shipNodeConfig) // + .withConnectClientsTo(TRUSTED) // + .withTrustedSkis(this.getTrustedSkis()); // + + this.eebusDevice = this.buildDevice(shipCommunication); + this.shipCommunication = shipCommunication; + setValue(this, BridgeEebus.ChannelId.OWN_SKI, shipCommunication.getOwnSki()); + setValue(this, BridgeEebus.ChannelId.INITIALIZE_FAILURE, false); + + } catch (Exception ex) { + this.log.error("Failed to initialize eebus bridge " + this.id(), ex); + setValue(this, BridgeEebus.ChannelId.INITIALIZE_FAILURE, true); + } + } + + protected Device buildDevice(ShipCommunication shipCommunication) { + var useCases = this.useCaseManager.createUseCases(); + + return Device.getBuilder() // + .withDeviceType(DeviceTypeEnumType.GENERIC) // + .withCommunication(shipCommunication) // + .withId("SPINE-DEVICE-ID") // + .withDiscoverDevices(true) // + .addEntity() // + .setType(EntityTypeEnumType.CEM) // + .withUseCases(useCases) // + .applyToDevice() // + .build(); + } + + private ShipNodeConfiguration createNodeConfig(Config config) { + return new ShipNodeConfiguration(// + Set.of(config.bindHost()), // + config.bindPort(), // + "/ship/", // + true, // + config.serviceID(), "local.", // + config.serviceInstance(), // + this.certificateStorage, // + "CN=" + config.serviceID() + ", O=OpenEMS", // + 40 * 365 // + ); + } + + @Override + public String[] getTrustedSkis() { + return this.peers.stream() // + .map(InternalEebusPeer::getSki) // + .toArray(String[]::new); + } + + @Override + public String debugLog() { + return String.format("Connections:%d|Peers:%d|%s", this.connectionsAmount, this.peers.size(), + this.useCaseManager.debugLog()); + } + + @Override + public ImmutableList getPeers() { + return ImmutableList.copyOf(this.peers); + } + + @Override + public EebusUseCaseManager getUseCaseManager() { + return this.useCaseManager; + } + + @Override + public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } + + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // + -> this.updateConnectionInfos(); + } + } + + private void updateConnectionInfos() { + if (this.shipCommunication == null) { + return; + } + + Comparator sorting = Comparator.comparing(ShipConnectionInfoSnapshot::getTrustLevel) + .reversed() + .thenComparing(Comparator.comparing(ShipConnectionInfoSnapshot::getConnectionDate).reversed()); + + var allConnections = this.shipCommunication.getConnectionInfos(); + this.connectionsAmount = allConnections.size(); + + var connectionInfos = allConnections.stream() // + .filter(ShipConnectionInfoSnapshot::isDataExchangeEstablished) // + .sorted(sorting) // + .toList(); + + for (var peer : this.peers) { + connectionInfos.stream() // + .filter(x -> x.getSki().equals(peer.getSki())) // + .findFirst() // + .ifPresent(peer::updateConnectionInfo); + } + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/Config.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/Config.java new file mode 100644 index 00000000000..1e285f97521 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/Config.java @@ -0,0 +1,37 @@ +package io.openems.edge.bridge.eebus; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(name = "Bridge EEBUS", description = "Provides a service for connecting to EEBUS devices.") +public @interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "eebus0"; + + @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 = "Binding Host", description = "Hostname on which the eebus server should listen.") + String bindHost() default ""; + + @AttributeDefinition(name = "Binding Port", description = "Port on which the Websocket server should listen.") + int bindPort() default 8085; + + @AttributeDefinition(name = "Service ID", description = "EEBUS Service ID. Internal name used for multicast. Should be unique and only contain characters [0-9A-Z-]") + String serviceID(); + + @AttributeDefinition(name = "Service Instance", description = "Friendly name for this EEBUS service. Visible to other EEBUS devices. Supports UTF-8, spaces, and up to 63 characters.") + String serviceInstance(); + + @AttributeDefinition(name = "TLS Certificate", description = "Self-signed TLS certificate for EEBUS. Automatically generated if left empty. Changing this value will also change the SKI.") + String tlsCertificate(); + + @AttributeDefinition(name = "Debug Mode", description = "Enable debug logging for MQTT communication") + boolean debugMode() default false; + + String webconsole_configurationFactory_nameHint() default "Bridge EEBUS [{id}]"; +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/ConfigCertificateStorage.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/ConfigCertificateStorage.java new file mode 100644 index 00000000000..7ab0c6f0cfe --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/ConfigCertificateStorage.java @@ -0,0 +1,179 @@ +package io.openems.edge.bridge.eebus; + +import org.openmuc.jeebus.ship.api.cert.CertificateInfo; +import org.openmuc.jeebus.ship.api.cert.CertificateStorage; +import org.openmuc.jeebus.ship.api.cert.CertificateStoreException; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.Optional; + +public class ConfigCertificateStorage implements CertificateStorage { + private final ConfigurationAdmin cm; + private final String servicePid; + + private static final String TLS_CERTIFICATE_CONFIG_KEY = "tlsCertificate"; + private static final byte[] CERT_HEADER = "--OPENEMS-TLS-CERT-V1--".getBytes(StandardCharsets.UTF_8); + + public ConfigCertificateStorage(ConfigurationAdmin cm, String servicePid) { + this.cm = cm; + this.servicePid = servicePid; + } + + private Configuration readConfiguration() throws IOException { + return this.cm.getConfiguration(this.servicePid); + } + + private Optional decodeCertificateInfo(String storedStr) throws CertificateStoreException { + if (storedStr == null || storedStr.isBlank()) { + return Optional.empty(); + } + + var bytes = Base64.getDecoder().decode(storedStr); + try (var reader = new DataInputStream(new ByteArrayInputStream(bytes))) { + return Optional.of(this.decodeCertificateInfo(reader)); + } catch (IOException ex) { + throw new CertificateStoreException("Invalid certificate data: Read failure", ex); + } + } + + private CertificateInfo decodeCertificateInfo(DataInputStream reader) throws IOException, CertificateStoreException { + var header = reader.readNBytes(CERT_HEADER.length); + if (!Arrays.equals(header, CERT_HEADER)) { + throw new CertificateStoreException("Invalid certificate data: too short", null); + } + + var privateKeyLen = reader.readInt(); + var privateKeyBytes = reader.readNBytes(privateKeyLen); + + var certificateLen = reader.readInt(); + var certificateBytes = reader.readNBytes(certificateLen); + + var privateKey = this.decodePrivateKey(privateKeyBytes); + var x509certificate = this.decodeX509Certificate(certificateBytes); + + return new CertificateInfo(privateKey, x509certificate); + } + + /** + * Decodes a PKCS#8 DER-encoded private key (EC algorithm). + * + * @param bytes the PKCS#8 encoded key bytes + * @return the decoded {@link PrivateKey} + * @throws CertificateStoreException if the key cannot be decoded + */ + private PrivateKey decodePrivateKey(byte[] bytes) throws CertificateStoreException { + try (var reader = new DataInputStream(new ByteArrayInputStream(bytes))) { + var algorithmLength = reader.readInt(); + var algorithm = new String(reader.readNBytes(algorithmLength), StandardCharsets.UTF_8); + + var privateKeyBytes = reader.readAllBytes(); + var keySpec = new PKCS8EncodedKeySpec(privateKeyBytes, algorithm); + + var keyFactory = KeyFactory.getInstance(algorithm); + return keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException ex) { + throw new CertificateStoreException("Invalid certificate data: failed to decode private key", ex); + } + } + + private X509Certificate decodeX509Certificate(byte[] bytes) throws CertificateStoreException { + try { + var certFactory = CertificateFactory.getInstance("X.509"); + return (java.security.cert.X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(bytes)); + } catch (CertificateException e) { + throw new CertificateStoreException("Invalid certificate data: failed to decode X.509 certificate", e); + } + } + + private String encodeCertificateInfoToString(CertificateInfo certificateInfo) throws IOException, CertificateEncodingException { + if (certificateInfo == null) { + return null; + } + + return Base64.getEncoder().encodeToString(this.encodeCertificateInfo(certificateInfo)); + } + + private byte[] encodeCertificateInfo(CertificateInfo certificateInfo) throws IOException, CertificateEncodingException { + var encodedPrivateKey = this.encodePrivateKey(certificateInfo.privateKey); + var encodedX509Cert = certificateInfo.certificate.getEncoded(); + + try (var out = new ByteArrayOutputStream()) { + try (var writer = new DataOutputStream(out)) { + writer.write(CERT_HEADER); + + writer.writeInt(encodedPrivateKey.length); + writer.write(encodedPrivateKey); + + writer.writeInt(encodedX509Cert.length); + writer.write(encodedX509Cert); + + return out.toByteArray(); + } + } + } + + private byte[] encodePrivateKey(PrivateKey privateKey) throws IOException { + try (var out = new ByteArrayOutputStream()) { + try (var writer = new DataOutputStream(out)) { + var algorithmAsBytes = privateKey.getAlgorithm().getBytes(StandardCharsets.UTF_8); + writer.writeInt(algorithmAsBytes.length); + writer.write(algorithmAsBytes); + + writer.write(privateKey.getEncoded()); + return out.toByteArray(); + } + } + } + + @Override + public synchronized Optional readCertificate() throws CertificateStoreException { + try { + var config = this.readConfiguration(); + var p = config.getProperties(); + + var tlsCertificateStr = (String) p.get(TLS_CERTIFICATE_CONFIG_KEY); + return this.decodeCertificateInfo(tlsCertificateStr); + } catch (IOException ex) { + throw new CertificateStoreException("Failed to read certificate from configuration", ex); + } + } + + @Override + public synchronized void saveCertificate(CertificateInfo certificateInfo) throws CertificateStoreException { + String encodedCertStr; + try { + encodedCertStr = this.encodeCertificateInfoToString(certificateInfo); + } catch (IOException | CertificateEncodingException ex) { + throw new CertificateStoreException("Failed to write certificate: Failed to encode certificate info", ex); + } + + try { + var config = this.readConfiguration(); + + var props = config.getProperties(); + props.put(TLS_CERTIFICATE_CONFIG_KEY, encodedCertStr); + + config.update(props); + } catch (IOException ex) { + throw new CertificateStoreException("Failed to save certificate to configuration", ex); + } + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/DelayedUpdater.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/DelayedUpdater.java new file mode 100644 index 00000000000..fc94482285a --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/DelayedUpdater.java @@ -0,0 +1,65 @@ +package io.openems.edge.bridge.eebus; + +import io.openems.common.function.Disposable; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class DelayedUpdater { + private final Runnable runnable; + private final Instant initializationTime; + private final Duration updateDelay; + private final Duration maxWaitBetweenUpdatesDelay; + private final Object updateLock = new Object(); + + private volatile Object updateToken = new Object(); + private Instant lastExecution; + + public DelayedUpdater(Runnable runnable, Duration initialDelay, Duration updateDelay, Duration maxWaitBetweenUpdatesDelay) { + this.runnable = runnable; + this.initializationTime = Instant.now(); + this.updateDelay = updateDelay; + this.maxWaitBetweenUpdatesDelay = maxWaitBetweenUpdatesDelay; + if (initialDelay != null) { + this.scheduleRun(initialDelay); + } + } + + private void scheduleRun(Duration delay) { + var myUpdateToken = new Object(); + this.updateToken = myUpdateToken; + + var forceExecutionTime = this.lastExecution != null // + ? this.lastExecution.plus(this.maxWaitBetweenUpdatesDelay) // + : this.initializationTime.plus(this.maxWaitBetweenUpdatesDelay); + + if (Instant.now().isAfter(forceExecutionTime)) { + CompletableFuture.runAsync(() -> this.delayedRun(myUpdateToken)); + } else { + CompletableFuture.delayedExecutor(delay.get(ChronoUnit.SECONDS), TimeUnit.SECONDS) + .execute(() -> this.delayedRun(myUpdateToken)); + } + } + + private void delayedRun(Object updateToken) { + synchronized (this.updateLock) { + if (this.updateToken != updateToken) { + return; + } + + this.lastExecution = Instant.now(); + this.runnable.run(); + } + } + + public void scheduleUpdate() { + this.scheduleRun(this.updateDelay); + } + + public void cancelPendingUpdate() { + this.updateToken = new Object(); + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/BridgeEebus.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/BridgeEebus.java new file mode 100644 index 00000000000..7670a71e788 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/BridgeEebus.java @@ -0,0 +1,57 @@ +package io.openems.edge.bridge.eebus.api; + +import com.google.common.collect.ImmutableList; +import io.openems.common.channel.AccessMode; +import io.openems.common.channel.Level; +import io.openems.common.channel.PersistencePriority; +import io.openems.common.channel.Unit; +import io.openems.common.types.OpenemsType; +import io.openems.edge.bridge.eebus.Config; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.metatype.annotations.Designate; + +import java.util.List; + +import static org.osgi.framework.BundlePermission.REQUIRE; + +public interface BridgeEebus extends OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + INITIALIZE_FAILURE(Doc.of(Level.FAULT)), + + OWN_SKI(Doc.of(OpenemsType.STRING) + .accessMode(AccessMode.READ_ONLY)), + + LPC_CURRENT_LIMIT(Doc.of(OpenemsType.LONG) + .unit(Unit.WATT) + .accessMode(AccessMode.READ_ONLY) + .persistencePriority(PersistencePriority.HIGH)), + + LPP_CURRENT_LIMIT(Doc.of(OpenemsType.LONG) + .unit(Unit.WATT) + .accessMode(AccessMode.READ_ONLY) + .persistencePriority(PersistencePriority.HIGH)), + + ; + + private final Doc doc; + + ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + ImmutableList getPeers(); + + String[] getTrustedSkis(); + + EebusUseCaseManager getUseCaseManager(); + +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/EebusPeer.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/EebusPeer.java new file mode 100644 index 00000000000..ca2f51d5813 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/EebusPeer.java @@ -0,0 +1,69 @@ +package io.openems.edge.bridge.eebus.api; + +import io.openems.common.channel.Level; +import io.openems.common.types.OpenemsType; +import io.openems.common.types.OptionsEnum; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; + +public interface EebusPeer extends OpenemsComponent { + + enum EebusPeerConnectionType implements OptionsEnum { + NOT_CONNECTED(0), + PEER_CONNECTED_TO_SERVER(1), + CLIENT_CONNECTION_TO_PEER(2), + ; + + private final int value; + + private EebusPeerConnectionType(int value) { + this.value = value; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public String getName() { + return this.name(); + } + + @Override + public OptionsEnum getUndefined() { + return NOT_CONNECTED; + } + } + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + + CONNECTION_FAILURE(Doc.of(Level.WARNING)), + + MISSING_TRUST(Doc.of(Level.WARNING)), + + TRUST_LEVEL(Doc.of(OpenemsType.INTEGER)), // + + REMOTE_IP(Doc.of(OpenemsType.STRING)), // + + CONNECTION_TYPE(Doc.of(EebusPeerConnectionType.values())), // + + ; + + private final Doc doc; + + ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + public String getSki(); + + public boolean isValid(); + +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/EebusUseCaseManager.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/EebusUseCaseManager.java new file mode 100644 index 00000000000..98829e58767 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/EebusUseCaseManager.java @@ -0,0 +1,13 @@ +package io.openems.edge.bridge.eebus.api; + +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.ILimitPowerConsumptionHandler; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.ILimitPowerProductionHandler; + +public interface EebusUseCaseManager { + void addLimitPowerProductionHandler(ILimitPowerProductionHandler handler); + void removeLimitPowerProductionHandler(ILimitPowerProductionHandler handler); + + void addLimitPowerConsumptionHandler(ILimitPowerConsumptionHandler handler); + void removeLimitPowerConsumptionHandler(ILimitPowerConsumptionHandler handler); + +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/package-info.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/package-info.java new file mode 100644 index 00000000000..019c591486f --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/api/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.bridge.eebus.api; \ No newline at end of file diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/EebusPeerImpl.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/EebusPeerImpl.java new file mode 100644 index 00000000000..fa4db2db021 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/EebusPeerImpl.java @@ -0,0 +1,77 @@ +package io.openems.edge.bridge.eebus.peer; + +import static io.openems.edge.common.channel.ChannelUtils.setValue; +import static org.osgi.service.component.annotations.ConfigurationPolicy.REQUIRE; + +import io.openems.edge.bridge.eebus.api.EebusPeer; +import org.openmuc.jeebus.ship.api.ShipConnectionInfoSnapshot; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; + +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Designate(ocd = PeerConfig.class, factory = true) +@Component(name = "Bridge.EEBUS.Device", // + immediate = true, // + configurationPolicy = REQUIRE // +) +public class EebusPeerImpl extends AbstractOpenemsComponent implements EebusPeer, OpenemsComponent, InternalEebusPeer { + private final Logger log = LoggerFactory.getLogger(EebusPeerImpl.class); + + private String ski; + + public EebusPeerImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + BridgeEebus.ChannelId.values() // + ); + } + + @Activate + protected void activate(ComponentContext context, PeerConfig config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + + this.ski = config.ski(); + + if (!this.isValid()) { + this.logError(this.log, "Configuration for eebus peer is invalid. Check if the SKI is correct."); + } + } + + @Override + public String getSki() { + return this.ski; + } + + @Override + public boolean isValid() { + //Original function, but not usable because of missing OSGI export: return KeyManagement.isValidSki(this.ski); + return this.ski.matches("[0-9a-fA-F]+") && this.ski.replaceAll("\\s+", "").length() == 40; + } + + public void updateConnectionInfo(ShipConnectionInfoSnapshot connectionInfo) { + if (connectionInfo == null) { + setValue(this, EebusPeer.ChannelId.CONNECTION_FAILURE, true); + return; + } + + setValue(this, EebusPeer.ChannelId.CONNECTION_FAILURE, false); + setValue(this, EebusPeer.ChannelId.CONNECTION_TYPE, this.mapConnectionType(connectionInfo.getConnectionType())); + setValue(this, EebusPeer.ChannelId.TRUST_LEVEL, connectionInfo.getTrustLevel()); + setValue(this, EebusPeer.ChannelId.MISSING_TRUST, connectionInfo.getTrustLevel() < 16); + setValue(this, EebusPeer.ChannelId.REMOTE_IP, connectionInfo.getRemoteIP()); + } + + private EebusPeerConnectionType mapConnectionType(ShipConnectionInfoSnapshot.ConnectionTypeEnum type) { + return switch (type) { + case CLIENT_CONNECTION_TO_PEER -> EebusPeerConnectionType.CLIENT_CONNECTION_TO_PEER; + case PEER_CONNECTED_TO_SERVER -> EebusPeerConnectionType.PEER_CONNECTED_TO_SERVER; + }; + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/InternalEebusPeer.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/InternalEebusPeer.java new file mode 100644 index 00000000000..74c90da2be9 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/InternalEebusPeer.java @@ -0,0 +1,9 @@ +package io.openems.edge.bridge.eebus.peer; + +import io.openems.edge.bridge.eebus.api.EebusPeer; +import org.openmuc.jeebus.ship.api.ShipConnectionInfoSnapshot; + +public interface InternalEebusPeer extends EebusPeer { + + void updateConnectionInfo(ShipConnectionInfoSnapshot connectionInfo); +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/PeerConfig.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/PeerConfig.java new file mode 100644 index 00000000000..6b7924b3b79 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/PeerConfig.java @@ -0,0 +1,26 @@ +package io.openems.edge.bridge.eebus.peer; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(name = "Bridge EEBUS Peer", description = "Provides a peer that communicates via eebus") +public @interface PeerConfig { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "eebuspeer0"; + + @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 = "SKI", description = "EEBUS SKI of this peer") + String ski(); + + @AttributeDefinition(name = "EEBUS Bridge-ID", description = "Unique ID of EEBUS Bridge Component") + String eebus_id() default "eebus0"; + + String webconsole_configurationFactory_nameHint() default "Bridge EEBUS Peer [{id}]"; + +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/package-info.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/package-info.java new file mode 100644 index 00000000000..b3acbaef723 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/peer/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.bridge.eebus.peer; \ No newline at end of file diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyBridgeEebus.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyBridgeEebus.java new file mode 100644 index 00000000000..23614ada291 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyBridgeEebus.java @@ -0,0 +1,64 @@ +package io.openems.edge.bridge.eebus.test; + +import com.google.common.collect.ImmutableList; +import io.openems.edge.bridge.eebus.Config; +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.bridge.eebus.api.EebusPeer; +import io.openems.edge.bridge.eebus.api.EebusUseCaseManager; +import io.openems.edge.bridge.eebus.usecase.EebusUseCaseManagerImpl; +import io.openems.edge.bridge.eebus.usecase.EebusUseCaseType; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.LimitPowerConsumptionUseCase; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.LimitPowerState; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.test.AbstractDummyOpenemsComponent; + +public class DummyBridgeEebus extends AbstractDummyOpenemsComponent implements BridgeEebus { + private final EebusUseCaseManagerImpl useCaseManager; + + private final EebusPeer dummyPeer; + + public DummyBridgeEebus(String id) { + super(id, // + OpenemsComponent.ChannelId.values(), // + BridgeEebus.ChannelId.values() // + ); + this.useCaseManager = new EebusUseCaseManagerImpl(this); + this.dummyPeer = new DummyEebusPeer("eebuspeer0"); + + super.activate(null, id, "", true); + } + + @Override + public ImmutableList getPeers() { + return ImmutableList.builder().add(this.dummyPeer).build(); + } + + @Override + public String[] getTrustedSkis() { + return new String[] { this.dummyPeer.getSki() }; + } + + @Override + public EebusUseCaseManager getUseCaseManager() { + return this.useCaseManager; + } + + @Override + protected DummyBridgeEebus self() { + return this; + } + + public void sendPowerConsumptionLimit(LimitPowerState state, DummyLimitation limit) { + this.useCaseManager.injectCommand(EebusUseCaseType.LIMIT_POWER_CONSUMPTION, + (LimitPowerConsumptionUseCase useCase) -> { + switch (limit) { + case DummyLimitation.DummyNoLimitation() -> useCase.handleLimit(state, null); + case DummyLimitation.DummyAbsoluteLimitation(var absoluteLimit) -> + useCase.handleLimit(state, (double) absoluteLimit); + case DummyLimitation.DummyPercentageLimitation(var percentage) -> + useCase.handleLimit(state, useCase.getNominalMax() * percentage); + } + }); + } + +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyEebusPeer.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyEebusPeer.java new file mode 100644 index 00000000000..9875dde16ec --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyEebusPeer.java @@ -0,0 +1,31 @@ +package io.openems.edge.bridge.eebus.test; + +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.bridge.eebus.api.EebusPeer; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.test.AbstractDummyOpenemsComponent; + +public class DummyEebusPeer extends AbstractDummyOpenemsComponent implements EebusPeer { + protected DummyEebusPeer(String id) { + super(// + id, // + OpenemsComponent.ChannelId.values(), // + BridgeEebus.ChannelId.values() // + ); + } + + @Override + public String getSki() { + return "DUMMY"; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected DummyEebusPeer self() { + return this; + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyLimitPowerProduction.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyLimitPowerProduction.java new file mode 100644 index 00000000000..58c4eca9c4e --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyLimitPowerProduction.java @@ -0,0 +1,9 @@ +package io.openems.edge.bridge.eebus.test; + +public class DummyLimitPowerProduction { + + public void setLimit() { + + } + +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyLimitation.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyLimitation.java new file mode 100644 index 00000000000..7063d74af95 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/DummyLimitation.java @@ -0,0 +1,12 @@ +package io.openems.edge.bridge.eebus.test; + +public sealed interface DummyLimitation { + record DummyNoLimitation() implements DummyLimitation { + } + + record DummyAbsoluteLimitation(int limit) implements DummyLimitation { + } + + record DummyPercentageLimitation(double percentage) implements DummyLimitation { + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/package-info.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/test/package-info.java new file mode 100644 index 00000000000..384e84d506a --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/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.bridge.eebus.test; \ No newline at end of file diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCase.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCase.java new file mode 100644 index 00000000000..6ebe19b5225 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCase.java @@ -0,0 +1,11 @@ +package io.openems.edge.bridge.eebus.usecase; + +import org.openmuc.jeebus.spine.spi.UseCase; + +public abstract class EebusUseCase { + public abstract boolean isInUse(); + + public abstract UseCase createUseCase(); + + public abstract boolean requiresReInit(); +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCaseManagerImpl.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCaseManagerImpl.java new file mode 100644 index 00000000000..7c8f99b9304 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCaseManagerImpl.java @@ -0,0 +1,99 @@ +package io.openems.edge.bridge.eebus.usecase; + +import java.util.EnumMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.openmuc.jeebus.spine.spi.UseCase; + +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.bridge.eebus.api.EebusUseCaseManager; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.LimitPowerConsumptionUseCase; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.LimitPowerProductionUseCase; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.ILimitPowerConsumptionHandler; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.ILimitPowerProductionHandler; + +public class EebusUseCaseManagerImpl implements EebusUseCaseManager { + private final BridgeEebus bridge; + protected Map useCases = new EnumMap<>(EebusUseCaseType.class); + protected boolean dirty = true; + + public EebusUseCaseManagerImpl(BridgeEebus bridge) { + this.bridge = bridge; + } + + public void addLimitPowerProductionHandler(ILimitPowerProductionHandler handler) { + this.addOrUpdate(EebusUseCaseType.LIMIT_POWER_PRODUCTION, + x -> x.addHandler(handler)); + } + + public void removeLimitPowerProductionHandler(ILimitPowerProductionHandler handler) { + this.removeOrUpdate(EebusUseCaseType.LIMIT_POWER_PRODUCTION, + x -> x.removeHandler(handler)); + } + + public void addLimitPowerConsumptionHandler(ILimitPowerConsumptionHandler handler) { + this.addOrUpdate(EebusUseCaseType.LIMIT_POWER_CONSUMPTION, + x -> x.addHandler(handler)); + } + + public void removeLimitPowerConsumptionHandler(ILimitPowerConsumptionHandler handler) { + this.removeOrUpdate(EebusUseCaseType.LIMIT_POWER_CONSUMPTION, + x -> x.removeHandler(handler)); + } + + public void injectCommand(EebusUseCaseType useCaseType, Consumer command) { + var useCase = this.getUseCaseOrNull(useCaseType); + if (useCase != null) { + command.accept(useCase); + } + } + + protected void addOrUpdate(EebusUseCaseType useCaseType, Consumer updateMethod) { + var useCase = this.getUseCaseOrNull(useCaseType); + if (useCase == null) { + useCase = (T) useCaseType.createInstance(this.bridge); + updateMethod.accept(useCase); + this.useCases.put(useCaseType, useCase); + this.dirty = true; + } else { + updateMethod.accept(useCase); + } + } + + protected void removeOrUpdate(EebusUseCaseType useCaseType, Consumer updateMethod) { + var useCase = this.getUseCaseOrNull(useCaseType); + if (useCase == null) { + return; + } + + updateMethod.accept(useCase); + if (!useCase.isInUse()) { + this.useCases.remove(useCaseType); + this.dirty = true; + } + } + + private T getUseCaseOrNull(EebusUseCaseType useCaseType) { + return (T) this.useCases.get(useCaseType); + } + + public UseCase[] createUseCases() { + this.dirty = false; + return this.useCases.values().stream().map(EebusUseCase::createUseCase).toArray(UseCase[]::new); + } + + public String debugLog() { + var shortNames = this.useCases.keySet().stream().map(EebusUseCaseType::getShortName).collect(Collectors.joining(",")); + if (shortNames.isEmpty()) { + return ""; + } + + return "UseCases:" + shortNames; + } + + public boolean isDirty() { + return this.dirty || this.useCases.values().stream().anyMatch(EebusUseCase::requiresReInit); + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCaseType.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCaseType.java new file mode 100644 index 00000000000..8d60e07d019 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/EebusUseCaseType.java @@ -0,0 +1,30 @@ +package io.openems.edge.bridge.eebus.usecase; + +import java.util.function.Function; + +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.LimitPowerConsumptionUseCase; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.LimitPowerProductionUseCase; + +public enum EebusUseCaseType { + LIMIT_POWER_PRODUCTION(LimitPowerProductionUseCase::new, "LPP"), // + LIMIT_POWER_CONSUMPTION(LimitPowerConsumptionUseCase::new, "LPC"), // + + ; + + private final Function factory; + private final String shortName; + + private EebusUseCaseType(Function factory, String shortName) { + this.factory = factory; + this.shortName = shortName; + } + + EebusUseCase createInstance(BridgeEebus bridge) { + return this.factory.apply(bridge); + } + + public String getShortName() { + return this.shortName; + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/LimitPowerConsumptionUseCase.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/LimitPowerConsumptionUseCase.java new file mode 100644 index 00000000000..4d7b898f3c5 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/LimitPowerConsumptionUseCase.java @@ -0,0 +1,101 @@ +package io.openems.edge.bridge.eebus.usecase.powerlimitation; + +import static io.openems.edge.common.channel.ChannelUtils.setValue; + +import java.util.ArrayList; +import java.util.List; + +import org.openmuc.jeebus.spine.spi.UseCase; +import org.openmuc.jeebus.spine.utils.datatypes.ScaledNumberWrapper; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.ActiveLimit; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.LimitationConfig; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.SimpleLimitationConfig; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.lpc.LpcCs; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.states.Event; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.states.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.bridge.eebus.usecase.EebusUseCase; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.ILimitPowerConsumptionHandler; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.LimitPowerState; + +public class LimitPowerConsumptionUseCase extends EebusUseCase { + private final BridgeEebus bridge; + private final List handlers = new ArrayList(); + private final Logger log = LoggerFactory.getLogger(LimitPowerConsumptionUseCase.class); + + private LimitationConfig configSnapshot; + + public LimitPowerConsumptionUseCase(BridgeEebus bridge) { + this.bridge = bridge; + } + + public long getNominalMax() { + return this.handlers.stream() // + .mapToLong(ILimitPowerConsumptionHandler::getNominalMaxConsumption) // + .max() // + .getAsLong(); + } + + private LimitationConfig createConfig() { + return new SimpleLimitationConfig(// + LimitationConfig.DEFAULT_FAILSAFE_DURATION_MIN, // + LimitationConfig.DEFAULT_BIG_POWER, // + LimitationConfig.DEFAULT_BIG_POWER.negate(), // + new ScaledNumberWrapper(this.getNominalMax(), 0)); + } + + private LimitPowerState mapState(State state) { + return LimitPowerState.valueOf(state.name()); + } + + public void handleLimit(LimitPowerState state, Double activeLimit) { + setValue(this.bridge, BridgeEebus.ChannelId.LPC_CURRENT_LIMIT, activeLimit); + + for (var eventHandler : this.handlers) { + try { + eventHandler.handleLimitPowerConsumption(state, activeLimit); + } catch (Exception ex) { + this.log.error("An exception occurred while calling handleLimitPowerConsumption() for listener " + + eventHandler.getClass().getName(), ex); + } + } + } + + protected void handleEebusEvent(Event event, State state, ActiveLimit activeLimit) { + this.log.info("Received eebus LPC signal | State: {}, ActiveLimit: {}", state, activeLimit); + + var mappedState = this.mapState(state); + this.handleLimit(mappedState, activeLimit != null ? activeLimit.getResultingValue() : null); + } + + public void addHandler(ILimitPowerConsumptionHandler handler) { + this.handlers.add(handler); + } + + public void removeHandler(ILimitPowerConsumptionHandler handler) { + this.handlers.remove(handler); + } + + @Override + public boolean isInUse() { + return !this.handlers.isEmpty(); + } + + @Override + public UseCase createUseCase() { + this.configSnapshot = this.createConfig(); + + var useCase = new LpcCs(this.configSnapshot); + useCase.addListener(this::handleEebusEvent); + return useCase; + } + + @Override + public boolean requiresReInit() { + return this.configSnapshot == null + || !this.createConfig().getNominalMax().isEqualTo(this.configSnapshot.getNominalMax()); + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/LimitPowerProductionUseCase.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/LimitPowerProductionUseCase.java new file mode 100644 index 00000000000..0727d1a6c4c --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/LimitPowerProductionUseCase.java @@ -0,0 +1,101 @@ +package io.openems.edge.bridge.eebus.usecase.powerlimitation; + +import static io.openems.edge.common.channel.ChannelUtils.setValue; + +import java.util.ArrayList; +import java.util.List; + +import org.openmuc.jeebus.spine.spi.UseCase; +import org.openmuc.jeebus.spine.utils.datatypes.ScaledNumberWrapper; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.ActiveLimit; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.LimitationConfig; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.SimpleLimitationConfig; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.lpp.LppCs; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.states.Event; +import org.openmuc.jeebus.usecase.powerlimitation.controllablesystem.states.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.bridge.eebus.usecase.EebusUseCase; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.ILimitPowerProductionHandler; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.LimitPowerState; + +public class LimitPowerProductionUseCase extends EebusUseCase { + private final BridgeEebus bridge; + private final List handlers = new ArrayList<>(); + private final Logger log = LoggerFactory.getLogger(LimitPowerProductionUseCase.class); + + private LimitationConfig configSnapshot; + + public LimitPowerProductionUseCase(BridgeEebus bridge) { + this.bridge = bridge; + } + + private long getNominalMax() { + return this.handlers.stream() // + .mapToLong(ILimitPowerProductionHandler::getNominalMaxProduction) // + .max() // + .getAsLong(); + } + + private LimitationConfig createConfig() { + return new SimpleLimitationConfig(// + LimitationConfig.DEFAULT_FAILSAFE_DURATION_MIN, // + LimitationConfig.DEFAULT_BIG_POWER, // + LimitationConfig.DEFAULT_BIG_POWER.negate(), // + new ScaledNumberWrapper(this.getNominalMax(), 0)); + } + + private LimitPowerState mapState(State state) { + return LimitPowerState.valueOf(state.name()); + } + + public void handleLimit(LimitPowerState state, Double activeLimit) { + setValue(this.bridge, BridgeEebus.ChannelId.LPP_CURRENT_LIMIT, activeLimit); + + for (var eventHandler : this.handlers) { + try { + eventHandler.handleLimitPowerProduction(state, activeLimit); + } catch (Exception ex) { + this.log.error("An exception occurred while calling handleLimitPowerProduction() for listener " + + eventHandler.getClass().getName(), ex); + } + } + } + + protected void handleEebusEvent(Event event, State state, ActiveLimit activeLimit) { + this.log.info("Received eebus LPP signal | State: {}, ActiveLimit: {}", state, activeLimit); + + var mappedState = this.mapState(state); + this.handleLimit(mappedState, activeLimit.getResultingValue()); + } + + public void addHandler(ILimitPowerProductionHandler handler) { + this.handlers.add(handler); + } + + public void removeHandler(ILimitPowerProductionHandler handler) { + this.handlers.remove(handler); + } + + @Override + public boolean isInUse() { + return !this.handlers.isEmpty(); + } + + @Override + public UseCase createUseCase() { + this.configSnapshot = this.createConfig(); + + var useCase = new LppCs(this.configSnapshot); + useCase.addListener(this::handleEebusEvent); + return useCase; + } + + @Override + public boolean requiresReInit() { + return this.configSnapshot == null + || !this.createConfig().getNominalMax().isEqualTo(this.configSnapshot.getNominalMax()); + } +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/ILimitPowerConsumptionHandler.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/ILimitPowerConsumptionHandler.java new file mode 100644 index 00000000000..6bcbc7f96fd --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/ILimitPowerConsumptionHandler.java @@ -0,0 +1,7 @@ +package io.openems.edge.bridge.eebus.usecase.powerlimitation.api; + +public interface ILimitPowerConsumptionHandler { + long getNominalMaxConsumption(); + + void handleLimitPowerConsumption(LimitPowerState state, Double currentLimitInW); +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/ILimitPowerProductionHandler.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/ILimitPowerProductionHandler.java new file mode 100644 index 00000000000..767980d1450 --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/ILimitPowerProductionHandler.java @@ -0,0 +1,7 @@ +package io.openems.edge.bridge.eebus.usecase.powerlimitation.api; + +public interface ILimitPowerProductionHandler { + long getNominalMaxProduction(); + + void handleLimitPowerProduction(LimitPowerState state, Double currentLimitInW); +} \ No newline at end of file diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/LimitPowerState.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/LimitPowerState.java new file mode 100644 index 00000000000..91a786104ce --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/LimitPowerState.java @@ -0,0 +1,38 @@ +package io.openems.edge.bridge.eebus.usecase.powerlimitation.api; + +public enum LimitPowerState { + /** + * Controllable System starts in "init" state after completion of its (re)start; + * CS limited by the Failsafe Consumption Active Power Limit according to + * [LPC-901/1] and [LPC-901/2]. The Active Power Consumption Limit SHALL be + * deactivated ([LPC-009/2]). + */ + INIT, + /** + * Controllable System is not limited, but still controlled by Actor Energy Guard + * (unlike state "unlimited/autonomous"). + * The Active Power Consumption Limit SHALL be deactivated ([LPC-009/2]). + */ + CONTROLLED, + /** + * Controllable System is in a limited state (controlled by the Actor Energy Guard) where a + * limited amount of power is consumed. The Active Power Consumption Limit SHALL be activated + * ([LPC-009/1]). + */ + LIMITED, + /** + * Controllable System is in "failsafe state" (not controlled by the Energy Guard) where it + * is limited by the failsafe limit. The Active Power Consumption Limit SHALL be deactivated ([LPC- + * 009/2]). + */ + FAILSAFE, + /** + * Controllable System is not limited and consumes power as if there would be no + * external power limitation available. + * The Active Power Consumption Limit SHALL be deactivated ([LPC-009/2]) + */ + AUTONOMOUS + + ; + +} diff --git a/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/package-info.java b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/package-info.java new file mode 100644 index 00000000000..7d3d6eb897b --- /dev/null +++ b/io.openems.edge.bridge.eebus/src/io/openems/edge/bridge/eebus/usecase/powerlimitation/api/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.bridge.eebus.usecase.powerlimitation.api; \ No newline at end of file diff --git a/io.openems.edge.bridge.eebus/test/.gitignore b/io.openems.edge.bridge.eebus/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.bridge.eebus/test/io/openems/edge/bridge/eebus/BridgeEebusTest.java b/io.openems.edge.bridge.eebus/test/io/openems/edge/bridge/eebus/BridgeEebusTest.java new file mode 100644 index 00000000000..cbbd1a4015d --- /dev/null +++ b/io.openems.edge.bridge.eebus/test/io/openems/edge/bridge/eebus/BridgeEebusTest.java @@ -0,0 +1,11 @@ +package io.openems.edge.bridge.eebus; + +import io.openems.edge.bridge.eebus.test.DummyBridgeEebus; +import org.junit.Test; + +public class BridgeEebusTest { + @Test + public void testDummyBridge() throws Exception { + var bridge = new DummyBridgeEebus("eebus0"); + } +} diff --git a/io.openems.edge.bridge.eebus/test/io/openems/edge/bridge/eebus/ConfigCertificateStorageTest.java b/io.openems.edge.bridge.eebus/test/io/openems/edge/bridge/eebus/ConfigCertificateStorageTest.java new file mode 100644 index 00000000000..8cee095ee78 --- /dev/null +++ b/io.openems.edge.bridge.eebus/test/io/openems/edge/bridge/eebus/ConfigCertificateStorageTest.java @@ -0,0 +1,36 @@ +package io.openems.edge.bridge.eebus; + +import io.openems.common.test.DummyConfigurationAdmin; +import org.junit.Test; +import org.openmuc.jeebus.ship.api.cert.CertificateStorage; +import org.openmuc.jeebus.ship.api.cert.CertificateStoreException; +import org.openmuc.jeebus.ship.node.KeyManagement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ConfigCertificateStorageTest { + + @Test + public void testStore() throws Exception { + var cm = new DummyConfigurationAdmin(); + var store = new ConfigCertificateStorage(cm, "test.pid"); + + assertTrue(store.readCertificate().isEmpty()); + this.addCertificateToStore(store); + assertTrue(store.readCertificate().isPresent()); + var cert = store.readCertificate().get(); + + var secondStore = new ConfigCertificateStorage(cm, "test.pid"); + assertTrue(secondStore.readCertificate().isPresent()); + + var secondCert = store.readCertificate().get(); + assertEquals(cert.certificate, secondCert.certificate); + assertEquals(cert.privateKey, secondCert.privateKey); + } + + private void addCertificateToStore(CertificateStorage storage) throws CertificateStoreException { + new KeyManagement(storage, "CN=Test", "dummy", 365); + } + +} 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 203be7d9b76..41815601dd8 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 @@ -981,6 +981,19 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .unit(CUMULATED_WATT_HOURS)// .persistencePriority(VERY_HIGH)), // + /** + * Symmetric Battery Inverter: Max Apparent Power. + * + *
    + *
  • Interface: Max Apparent Power (origin: SymmetricBatteryInverter)) + *
  • Type: Integer + *
  • Unit: VA + *
+ */ + BATTERY_INVERTER_MAX_APPARENT_POWER(Doc.of(INTEGER)// + .unit(VOLT_AMPERE)// + .persistencePriority(VERY_HIGH)), // + /** * Is there any Component Info/Warning/Fault that is getting ignored/hidden * because of the 'ignoreStateComponents' configuration setting?. 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 37be62e4224..7d9407ab5be 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 @@ -39,7 +39,7 @@ import io.openems.edge.common.meta.Meta; import io.openems.edge.common.sum.Sum; import io.openems.edge.controller.api.Controller; -import io.openems.edge.controller.ess.ripplecontrolreceiver.ControllerEssRippleControlReceiver; +import io.openems.edge.controller.ess.ripplecontrolreceiver.PowerProductionLimiter; import io.openems.edge.energy.api.EnergySchedulable; import io.openems.edge.energy.api.handler.EnergyScheduleHandler; import io.openems.edge.ess.api.ManagedSymmetricEss; @@ -115,7 +115,7 @@ public class ControllerEssGridOptimizedChargeImpl extends AbstractOpenemsCompone protected ElectricityMeter meter; @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) - protected volatile ControllerEssRippleControlReceiver rcr; + protected volatile PowerProductionLimiter powerProductionLimiter; @Reference private ConfigurationAdmin cm; @@ -486,9 +486,10 @@ public EnergyScheduleHandler getEnergyScheduleHandler() { private void updateMaximumSellToGridPower() { final var gridSellHardLimit = this.meta.getGridSellHardLimit(); final Integer dynamicGridFeedInLimit; - var maxApparentPower = this.ess.getMaxApparentPower(); - if (this.rcr != null && this.rcr.isEnabled() && maxApparentPower.isDefined()) { - dynamicGridFeedInLimit = this.rcr.getDynamicGridFeedInLimit(maxApparentPower.get()); + if (this.powerProductionLimiter != null) { + this.ess.getMaxApparentPower() + .ifPresent(power -> this.powerProductionLimiter.setMaxNominalProductionPower(power)); + dynamicGridFeedInLimit = this.powerProductionLimiter.getGridFeedInLimit(); } else { dynamicGridFeedInLimit = null; } 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 0deb6f2d623..fa63ac3d669 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 @@ -34,6 +34,7 @@ import java.util.Map; import java.util.Optional; +import io.openems.edge.controller.ess.ripplecontrolreceiver.test.DummyPowerProductionLimiter; import org.junit.Ignore; import org.junit.Test; @@ -50,7 +51,6 @@ import io.openems.edge.common.test.Plot; import io.openems.edge.common.test.Plot.AxisFormat; import io.openems.edge.common.test.Plot.Data; -import io.openems.edge.controller.ess.ripplecontrolreceiver.test.DummyRippleControlReceiver; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.test.DummyHybridEss; @@ -65,7 +65,7 @@ public class ControllerEssGridOptimizedChargeImplTest { // Components private static final DummyMeta META = new DummyMeta().withGridSellHardLimit(7000); - private static final DummyRippleControlReceiver RCR = new DummyRippleControlReceiver("rcr0"); + private static final DummyPowerProductionLimiter RCR = new DummyPowerProductionLimiter("rcr0"); 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"); diff --git a/io.openems.edge.controller.ess.limiter14a/bnd.bnd b/io.openems.edge.controller.ess.limiter14a/bnd.bnd index dd10c2715b1..18ef3586504 100644 --- a/io.openems.edge.controller.ess.limiter14a/bnd.bnd +++ b/io.openems.edge.controller.ess.limiter14a/bnd.bnd @@ -6,6 +6,7 @@ Bundle-Version: 1.0.0.${tstamp} -buildpath: \ ${buildpath},\ io.openems.common,\ + io.openems.edge.bridge.eebus,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ io.openems.edge.ess.api,\ diff --git a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/Config.java b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/Config.java new file mode 100644 index 00000000000..b3d927a6519 --- /dev/null +++ b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/Config.java @@ -0,0 +1,29 @@ +package io.openems.edge.controller.ess.limiter14a.eebus; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "Controller Ess Limiter §14a - EEBUS", // + description = "Established by law (for Germany), this controller lowers active power to -4200W in response to grid operator limitations, aiming to alleviate load on transformers. Signal is received by ethernet over EEBUS.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "ctrlEssLimiter14a0"; + + @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 = "Ess-ID", description = "ID of Ess.") + String ess_id() default "ess0"; + + @AttributeDefinition(name = "Eebus-ID", description = "ID of the EEBUS Bridge.") + String eebus_id() default "eebus0"; + + String webconsole_configurationFactory_nameHint() default "Controller Ess Limiter §14a - EEBUS [{id}]"; + + +} diff --git a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/ControllerEssLimiter14aEebus.java b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/ControllerEssLimiter14aEebus.java new file mode 100644 index 00000000000..164651fd688 --- /dev/null +++ b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/ControllerEssLimiter14aEebus.java @@ -0,0 +1,65 @@ +package io.openems.edge.controller.ess.limiter14a.eebus; + +import static io.openems.common.channel.PersistencePriority.HIGH; + +import io.openems.common.types.OpenemsType; +import io.openems.common.types.OptionsEnum; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.controller.ess.limiter14a.ControllerEssLimiter14a; + +public interface ControllerEssLimiter14aEebus extends ControllerEssLimiter14a { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + + FAILSAFE_LIMIT(Doc.of(OpenemsType.INTEGER)// + .persistencePriority(HIGH)), // + + RESTRICTION_MODE_REASON(Doc.of(RestrictionModeReason.values())// + .persistencePriority(HIGH)), // + + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + + } + + public enum RestrictionModeReason implements OptionsEnum { + NO_LIMIT(-1, "NoLimit"), // + ACTIVE_FAILSAFE(1, "ActiveFailsafe"), // + LIMITED(2, "Limited"), // + ; + + private final int value; + private final String name; + + private RestrictionModeReason(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 NO_LIMIT; + } + } + +} diff --git a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/ControllerEssLimiter14aEebusImpl.java b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/ControllerEssLimiter14aEebusImpl.java new file mode 100644 index 00000000000..1d80b44a81d --- /dev/null +++ b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/ControllerEssLimiter14aEebusImpl.java @@ -0,0 +1,165 @@ +package io.openems.edge.controller.ess.limiter14a.eebus; + +import io.openems.common.referencetarget.GenerateTargetsFromReferences; +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.ILimitPowerConsumptionHandler; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.LimitPowerState; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.metatype.annotations.Designate; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.sum.GridMode; +import io.openems.edge.common.sum.Sum; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.ess.limiter14a.ControllerEssLimiter14a; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.openems.edge.common.channel.ChannelUtils.setValue; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.Ess.Limiter14a.Eebus", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +@GenerateTargetsFromReferences({"ess", "eebusBridge"}) +public class ControllerEssLimiter14aEebusImpl extends AbstractOpenemsComponent implements // + ControllerEssLimiter14aEebus, ControllerEssLimiter14a, Controller, OpenemsComponent, TimedataProvider, ILimitPowerConsumptionHandler { + + @Reference + private Sum sum; + + @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private volatile Timedata timedata = null; + + @Reference + private ConfigurationAdmin cm; + + @Reference + private ComponentManager componentManager; + + @Reference(// + target = "(&(id=${config.eebus_id})(enabled=true))", + policy = ReferencePolicy.STATIC, // + policyOption = ReferencePolicyOption.GREEDY, // + cardinality = ReferenceCardinality.MANDATORY // + ) + private volatile BridgeEebus eebusBridge; + + @Reference(target = "(&(id=${config.ess_id})(enabled=true))", // + policy = ReferencePolicy.STATIC, // + policyOption = ReferencePolicyOption.GREEDY, // + cardinality = ReferenceCardinality.MANDATORY) + private volatile ManagedSymmetricEss ess; + + private Double currentLimitInW; + + private final CalculateActiveTime cumulatedRestrictionTime = new CalculateActiveTime(this, + ControllerEssLimiter14a.ChannelId.CUMULATED_RESTRICTION_TIME); + + private final Logger logger = LoggerFactory.getLogger(ControllerEssLimiter14aEebusImpl.class); + + public ControllerEssLimiter14aEebusImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + ControllerEssLimiter14a.ChannelId.values(), // + ControllerEssLimiter14aEebus.ChannelId.values() // + ); + } + + @Activate + protected void activate(ComponentContext context, Config config) throws OpenemsNamedException { + super.activate(context, config.id(), config.alias(), config.enabled()); + this.applyConfig(config); + + this.eebusBridge.getUseCaseManager().addLimitPowerConsumptionHandler(this); + } + + @Modified + protected void modified(ComponentContext context, Config config) throws OpenemsNamedException { + super.modified(context, config.id(), config.alias(), config.enabled()); // + this.applyConfig(config); + } + + protected void applyConfig(Config config) throws OpenemsNamedException { + + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + this.eebusBridge.getUseCaseManager().removeLimitPowerConsumptionHandler(this); + } + + @Override + public void run() throws OpenemsNamedException { + if (this.isLimitActive()) { + this.ess.setActivePowerGreaterOrEquals(this.currentLimitInW.intValue()); + this.cumulatedRestrictionTime.update(true); + } else { + this.cumulatedRestrictionTime.update(false); + } + } + + public boolean isLimitActive() { + return this.sum.getGridMode() == GridMode.ON_GRID && this.currentLimitInW != null; + } + + @Override + public String debugLog() { + if (this.currentLimitInW == null) { + return "No limit"; + } + + return "Limit: " + this.currentLimitInW + "W"; + } + + @Override + public Timedata getTimedata() { + return this.timedata; + } + + @Override + public long getNominalMaxConsumption() { + return 0; + } + + @Override + public void handleLimitPowerConsumption(LimitPowerState state, Double currentLimitInW) { + this.logInfo(this.logger, "Received eebus limit: " + currentLimitInW); + this.currentLimitInW = currentLimitInW; + + var isRestrictionActive = currentLimitInW != null; + setValue(this, ControllerEssLimiter14a.ChannelId.RESTRICTION_MODE, isRestrictionActive); + this.setRestrictionReason(state, isRestrictionActive); + } + + private void setRestrictionReason(LimitPowerState state, boolean isRestrictionActive) { + if (!isRestrictionActive) { + setValue(this, ControllerEssLimiter14aEebus.ChannelId.RESTRICTION_MODE_REASON, RestrictionModeReason.NO_LIMIT); + } else if (state == LimitPowerState.FAILSAFE) { + setValue(this, ControllerEssLimiter14aEebus.ChannelId.RESTRICTION_MODE_REASON, RestrictionModeReason.ACTIVE_FAILSAFE); + } else { + setValue(this, ControllerEssLimiter14aEebus.ChannelId.RESTRICTION_MODE_REASON, RestrictionModeReason.LIMITED); + } + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/package-info.java b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/package-info.java new file mode 100644 index 00000000000..f3e3fff4b07 --- /dev/null +++ b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/eebus/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.limiter14a.eebus; \ No newline at end of file diff --git a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/Config.java b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/Config.java similarity index 80% rename from io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/Config.java rename to io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/Config.java index c2768e430d8..d7602084602 100644 --- a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/Config.java +++ b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/Config.java @@ -1,11 +1,11 @@ -package io.openems.edge.controller.ess.limiter14a; +package io.openems.edge.controller.ess.limiter14a.relaissignal; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @ObjectClassDefinition(// - name = "Controller Ess Limiter §14a", // - description = "Established by law (for Germany), this controller lowers active power to -4200W in response to grid operator limitations, aiming to alleviate load on transformers.") + name = "Controller Ess Limiter §14a - Relais", // + description = "Established by law (for Germany), this controller lowers active power to -4200W in response to grid operator limitations, aiming to alleviate load on transformers. Signal is received by relais channels") @interface Config { @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") @@ -23,6 +23,6 @@ @AttributeDefinition(name = "Input Channel", description = "When receiving a signal, this channel triggers the execution of the limitation.") String inputChannelAddress(); - String webconsole_configurationFactory_nameHint() default "Controller Ess Limiter §14a [{id}]"; - + String webconsole_configurationFactory_nameHint() default "Controller Ess Limiter §14a - Relais [{id}]"; + } \ No newline at end of file diff --git a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/ControllerEssLimiter14aRelaisSignal.java b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/ControllerEssLimiter14aRelaisSignal.java new file mode 100644 index 00000000000..30753780d76 --- /dev/null +++ b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/ControllerEssLimiter14aRelaisSignal.java @@ -0,0 +1,6 @@ +package io.openems.edge.controller.ess.limiter14a.relaissignal; + +import io.openems.edge.controller.ess.limiter14a.ControllerEssLimiter14a; + +public interface ControllerEssLimiter14aRelaisSignal extends ControllerEssLimiter14a { +} 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/relaissignal/ControllerEssLimiter14aRelaisSignalImpl.java similarity index 65% rename from io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/ControllerEssLimiter14aImpl.java rename to io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/ControllerEssLimiter14aRelaisSignalImpl.java index 2911d12c9b9..8f4119b4801 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/relaissignal/ControllerEssLimiter14aRelaisSignalImpl.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.ess.limiter14a; +package io.openems.edge.controller.ess.limiter14a.relaissignal; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -6,6 +6,7 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.ConfigurationPolicy; import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; @@ -21,6 +22,7 @@ import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.sum.Sum; import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.ess.limiter14a.ControllerEssLimiter14a; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; @@ -32,8 +34,8 @@ immediate = true, // configurationPolicy = ConfigurationPolicy.REQUIRE // ) -public class ControllerEssLimiter14aImpl extends AbstractOpenemsComponent implements // - ControllerEssLimiter14a, Controller, OpenemsComponent, TimedataProvider { +public class ControllerEssLimiter14aRelaisSignalImpl extends AbstractOpenemsComponent implements // + ControllerEssLimiter14aRelaisSignal, ControllerEssLimiter14a, Controller, OpenemsComponent, TimedataProvider { @Reference private Sum sum; @@ -47,7 +49,10 @@ public class ControllerEssLimiter14aImpl extends AbstractOpenemsComponent implem @Reference private ComponentManager componentManager; - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + @Reference(target = "(&(id=${config.ess_id})(enabled=true))", // + policy = ReferencePolicy.STATIC, // + policyOption = ReferencePolicyOption.GREEDY, // + cardinality = ReferenceCardinality.MANDATORY) private ManagedSymmetricEss ess; private ChannelAddress inputChannelAddress; @@ -55,7 +60,7 @@ public class ControllerEssLimiter14aImpl extends AbstractOpenemsComponent implem private final CalculateActiveTime cumulatedRestrictionTime = new CalculateActiveTime(this, ControllerEssLimiter14a.ChannelId.CUMULATED_RESTRICTION_TIME); - public ControllerEssLimiter14aImpl() { + public ControllerEssLimiter14aRelaisSignalImpl() { super(// OpenemsComponent.ChannelId.values(), // Controller.ChannelId.values(), // @@ -64,13 +69,19 @@ public ControllerEssLimiter14aImpl() { } @Activate - private void activate(ComponentContext context, Config config) throws OpenemsNamedException { + protected void activate(ComponentContext context, Config config) throws OpenemsNamedException { super.activate(context, config.id(), config.alias(), config.enabled()); - this.inputChannelAddress = ChannelAddress.fromString(config.inputChannelAddress()); + this.applyConfig(config); + } - if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", config.ess_id())) { - return; - } + @Modified + protected void modified(ComponentContext context, Config config) throws OpenemsNamedException { + super.modified(context, config.id(), config.alias(), config.enabled()); // + this.applyConfig(config); + } + + protected void applyConfig(Config config) throws OpenemsNamedException { + this.inputChannelAddress = ChannelAddress.fromString(config.inputChannelAddress()); } @Deactivate @@ -80,16 +91,22 @@ protected void deactivate() { @Override public void run() throws OpenemsNamedException { - BooleanReadChannel inputChannel = this.componentManager.getChannel(this.inputChannelAddress); - var onGrid = this.sum.getGridMode() != GridMode.OFF_GRID; - // 0/1 is reversed on relays board - var isActive = onGrid && !inputChannel.value().orElse(true); - if (isActive) { + var isLimitActive = this.determinateIfLimitIsActive(); + if (isLimitActive) { this.ess.setActivePowerGreaterOrEquals(ESS_LIMIT_14A_ENWG); } - this._setRestrictionMode(isActive); - this.cumulatedRestrictionTime.update(isActive); + this._setRestrictionMode(isLimitActive); + this.cumulatedRestrictionTime.update(isLimitActive); + } + + protected boolean determinateIfLimitIsActive() throws OpenemsNamedException { + if (this.sum.getGridMode() != GridMode.ON_GRID) { + return false; + } + + BooleanReadChannel inputChannel = this.componentManager.getChannel(this.inputChannelAddress); + return !inputChannel.value().orElse(true); // 0/1 is reversed on relays board } @Override diff --git a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/package-info.java b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/package-info.java new file mode 100644 index 00000000000..66df0c59ec7 --- /dev/null +++ b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/relaissignal/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.limiter14a.relaissignal; \ No newline at end of file diff --git a/io.openems.edge.controller.ess.limiter14a/test/io/openems/edge/controller/ess/limiter14a/MyConfig.java b/io.openems.edge.controller.ess.limiter14a/test/io/openems/edge/controller/ess/limiter14a/MyConfig.java index 028564e3ea1..8c2892f4e98 100644 --- a/io.openems.edge.controller.ess.limiter14a/test/io/openems/edge/controller/ess/limiter14a/MyConfig.java +++ b/io.openems.edge.controller.ess.limiter14a/test/io/openems/edge/controller/ess/limiter14a/MyConfig.java @@ -1,6 +1,7 @@ package io.openems.edge.controller.ess.limiter14a; import io.openems.common.test.AbstractComponentConfig; +import io.openems.edge.controller.ess.limiter14a.relaissignal.Config; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/bnd.bnd b/io.openems.edge.controller.ess.ripplecontrolreceiver/bnd.bnd index 9ed60015668..7e4fe06c6ef 100644 --- a/io.openems.edge.controller.ess.ripplecontrolreceiver/bnd.bnd +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/bnd.bnd @@ -6,6 +6,7 @@ Bundle-Version: 1.0.0.${tstamp} -buildpath: \ ${buildpath},\ io.openems.common,\ + io.openems.edge.bridge.eebus,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ io.openems.edge.timedata.api,\ diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/ControllerEssRippleControlReceiver.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/ControllerEssRippleControlReceiver.java deleted file mode 100644 index 907b74af4c8..00000000000 --- a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/ControllerEssRippleControlReceiver.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.openems.edge.controller.ess.ripplecontrolreceiver; - -import static io.openems.common.channel.PersistencePriority.HIGH; -import static io.openems.common.types.OpenemsType.LONG; - -import io.openems.common.channel.Unit; -import io.openems.edge.common.channel.Channel; -import io.openems.edge.common.channel.Doc; -import io.openems.edge.common.channel.value.Value; -import io.openems.edge.common.component.OpenemsComponent; -import io.openems.edge.controller.api.Controller; - -public interface ControllerEssRippleControlReceiver extends Controller, OpenemsComponent { - - public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - - RESTRICTION_MODE(Doc.of(EssRestrictionLevel.values())// - .persistencePriority(HIGH)), // - - CUMULATED_RESTRICTION_TIME(Doc.of(LONG)// - .unit(Unit.CUMULATED_SECONDS)// - .persistencePriority(HIGH)); // - - private final Doc doc; - - ChannelId(Doc doc) { - this.doc = doc; - } - - @Override - public Doc doc() { - return this.doc; - } - - } - - /** - * Gets the Channel for {@link ChannelId#RESTRICTION_MODE}. - * - * @return the Channel - */ - default Channel getRestrictionModeChannel() { - return this.channel(ChannelId.RESTRICTION_MODE); - } - - /** - * Gets the restriction mode. See {@link ChannelId#RESTRICTION_MODE}. - * - * @return the Channel {@link Value} - */ - default EssRestrictionLevel getRestrictionMode() { - return this.getRestrictionModeChannel().value().asEnum(); - } - - /** - * Sets the restriction mode. See {@link ChannelId#RESTRICTION_MODE}. - * - * @param value the value as {@link EssRestrictionLevel}. - */ - default void _setRestrictionMode(EssRestrictionLevel value) { - this.getRestrictionModeChannel().setNextValue(value); - } - - /** - * Represents the current {@link EssRestrictionLevel}. - * - *

- * This is determined by the external ripple control signals. - *

- * - * @return the current restriction level. - */ - EssRestrictionLevel essRestrictionLevel(); - - /** - * Calculates the currently allowed grid feed-in power. This is determined as - * the minimum of: - *
    - *
  • the maximum apparent power multiplied by the limitation factor
  • - *
  • the raw limitation value
  • - *
- * - * @param maxApparentPower the maximum apparent power in VA. - * @return the allowed grid feed-in power in VA. - */ - default int getDynamicGridFeedInLimit(int maxApparentPower) { - return (int) (maxApparentPower * this.essRestrictionLevel().getLimitationFactor()); - } -} diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/PowerProductionLimiter.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/PowerProductionLimiter.java new file mode 100644 index 00000000000..208ea96d2f4 --- /dev/null +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/PowerProductionLimiter.java @@ -0,0 +1,12 @@ +package io.openems.edge.controller.ess.ripplecontrolreceiver; + +public interface PowerProductionLimiter { + void setMaxNominalProductionPower(int maxNominalProductionPowerInW); + + /** + * Returns how much watt we can feed into the grid. + * + * @return Limit in Wh + */ + Integer getGridFeedInLimit(); +} diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/PowerProductionLimiterComponent.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/PowerProductionLimiterComponent.java new file mode 100644 index 00000000000..7f175d626e9 --- /dev/null +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/PowerProductionLimiterComponent.java @@ -0,0 +1,37 @@ +package io.openems.edge.controller.ess.ripplecontrolreceiver; + +import io.openems.common.channel.Unit; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; + +import static io.openems.common.channel.PersistencePriority.HIGH; +import static io.openems.common.types.OpenemsType.INTEGER; +import static io.openems.common.types.OpenemsType.LONG; + +public interface PowerProductionLimiterComponent extends PowerProductionLimiter, OpenemsComponent { + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + + RESTRICTION(Doc.of(INTEGER) + .unit(Unit.WATT) + .persistencePriority(HIGH)), // + + CUMULATED_RESTRICTION_TIME(Doc.of(LONG)// + .unit(Unit.CUMULATED_SECONDS)// + .persistencePriority(HIGH)); // + + private final Doc doc; + + ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + public default Integer getGridFeedInLimit() { + return (Integer) this.channel(ChannelId.RESTRICTION).value().orElse(null); + } +} diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/Config.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/Config.java new file mode 100644 index 00000000000..8e5e2ff58e3 --- /dev/null +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/Config.java @@ -0,0 +1,25 @@ +package io.openems.edge.controller.ess.ripplecontrolreceiver.eebus; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "Controller Ess Ripple Control Receiver, EEBUS", // + description = "Controller to optimize energy distribution during peak hours by reducing the inverter output to 0, 30 or 60 percent.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "limiter0"; + + @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 = "Eebus-ID", description = "ID of the EEBUS Bridge.") + String eebus_id() default "eebus0"; + + String webconsole_configurationFactory_nameHint() default "Controller Ess Ripple Control Receiver [{id}], EEEBUS"; + +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/EebusPowerProductionLimiter.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/EebusPowerProductionLimiter.java new file mode 100644 index 00000000000..fdc53c1b75b --- /dev/null +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/EebusPowerProductionLimiter.java @@ -0,0 +1,65 @@ +package io.openems.edge.controller.ess.ripplecontrolreceiver.eebus; + +import io.openems.common.channel.Level; +import io.openems.common.types.OpenemsType; +import io.openems.common.types.OptionsEnum; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.controller.ess.ripplecontrolreceiver.PowerProductionLimiterComponent; + +import static io.openems.common.channel.PersistencePriority.HIGH; + +public interface EebusPowerProductionLimiter extends PowerProductionLimiterComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + RESTRICTION_REASON(Doc.of(RestrictionModeReason.values())//), + .persistencePriority(HIGH)), // + + FAILSAFE_LIMIT(Doc.of(OpenemsType.INTEGER)// + .persistencePriority(HIGH)), // + + UPDATE_FAILURE(Doc.of(Level.FAULT)), // + ; + + private final Doc doc; + + ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + public enum RestrictionModeReason implements OptionsEnum { + NO_LIMIT(-1, "NoLimit"), // + ACTIVE_FAILSAFE(1, "ActiveFailsafe"), // + LIMITED(2, "Limited"), // + ; + + private final int value; + private final String name; + + private RestrictionModeReason(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 NO_LIMIT; + } + } + +} diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/EebusPowerProductionLimiterImpl.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/EebusPowerProductionLimiterImpl.java new file mode 100644 index 00000000000..869525de8b3 --- /dev/null +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/eebus/EebusPowerProductionLimiterImpl.java @@ -0,0 +1,162 @@ +package io.openems.edge.controller.ess.ripplecontrolreceiver.eebus; + +import static io.openems.edge.common.channel.ChannelUtils.setValue; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventHandler; +import org.osgi.service.metatype.annotations.Designate; + +import io.openems.common.exceptions.OpenemsError; +import io.openems.common.referencetarget.GenerateTargetsFromReferences; +import io.openems.edge.bridge.eebus.api.BridgeEebus; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.ILimitPowerProductionHandler; +import io.openems.edge.bridge.eebus.usecase.powerlimitation.api.LimitPowerState; +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.sum.GridMode; +import io.openems.edge.common.sum.Sum; +import io.openems.edge.controller.ess.ripplecontrolreceiver.PowerProductionLimiter; +import io.openems.edge.controller.ess.ripplecontrolreceiver.PowerProductionLimiterComponent; +import io.openems.edge.timedata.api.Timedata; +import io.openems.edge.timedata.api.TimedataProvider; +import io.openems.edge.timedata.api.utils.CalculateActiveTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.Ess.RippleControlReceiver.Eebus", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +@GenerateTargetsFromReferences({ "eebusBridge" }) +public class EebusPowerProductionLimiterImpl extends AbstractOpenemsComponent + implements EebusPowerProductionLimiter, PowerProductionLimiterComponent, PowerProductionLimiter, + OpenemsComponent, TimedataProvider, EventHandler, ILimitPowerProductionHandler { + + @Reference + private Sum sum; + + @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private volatile Timedata timedata = null; + + @Reference(// + target = "(&(id=${config.eebus_id})(enabled=true))", policy = ReferencePolicy.STATIC, // + policyOption = ReferencePolicyOption.GREEDY, // + cardinality = ReferenceCardinality.MANDATORY // + ) + private volatile BridgeEebus eebusBridge; + + @Reference + private ConfigurationAdmin cm; + + private Integer maxNominalProductionPower = 15_000; + private Integer eebusLimit; + + private final CalculateActiveTime cumulatedRestrictionTime = new CalculateActiveTime(this, + PowerProductionLimiterComponent.ChannelId.CUMULATED_RESTRICTION_TIME); + + private final Logger log = LoggerFactory.getLogger(EebusPowerProductionLimiterImpl.class); + + public EebusPowerProductionLimiterImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + PowerProductionLimiterComponent.ChannelId.values(), // + EebusPowerProductionLimiter.ChannelId.values() // + ); + } + + @Activate + private void activate(ComponentContext context, Config config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + this.eebusBridge.getUseCaseManager().addLimitPowerProductionHandler(this); + } + + @Modified + protected void modified(ComponentContext context, Config config) { + super.modified(context, config.id(), config.alias(), config.enabled()); // + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + this.eebusBridge.getUseCaseManager().removeLimitPowerProductionHandler(this); + } + + @Override + public Timedata getTimedata() { + return this.timedata; + } + + @Override + public void setMaxNominalProductionPower(int maxNominalProductionPowerInW) { + this.maxNominalProductionPower = maxNominalProductionPowerInW; + } + + @Override + public long getNominalMaxProduction() { + return this.maxNominalProductionPower; + } + + @Override + public void handleLimitPowerProduction(LimitPowerState state, Double currentLimitInW) { + this.logInfo(this.log, "Received eebus limit: " + currentLimitInW); + this.eebusLimit = currentLimitInW != null ? currentLimitInW.intValue() : null; + } + + protected void run() { + try { + if (this.sum.getGridMode() != GridMode.ON_GRID) { + setValue(this, PowerProductionLimiterComponent.ChannelId.RESTRICTION, null); + this.cumulatedRestrictionTime.update(false); + return; + } + + setValue(this, PowerProductionLimiterComponent.ChannelId.RESTRICTION, this.eebusLimit); + this.cumulatedRestrictionTime.update(this.eebusLimit != null); + } catch (Exception ex) { + // TODO + setValue(this, EebusPowerProductionLimiter.ChannelId.UPDATE_FAILURE, true); + } + } + + @Override + public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } + + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: + this.run(); + break; + } + } + + @Override + public String debugLog() { + return "MaxPowerConsumption: " + this.formatInteger(this.maxNominalProductionPower) // + + "|Limit: " + this.formatInteger(this.eebusLimit); + } + + private String formatInteger(Integer value) { + if (value == null) { + return "None"; + } else { + return value.toString(); + } + } +} diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/Config.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/Config.java similarity index 88% rename from io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/Config.java rename to io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/Config.java index b91374d3fbe..0b2da55ee87 100644 --- a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/Config.java +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/Config.java @@ -1,15 +1,15 @@ -package io.openems.edge.controller.ess.ripplecontrolreceiver; +package io.openems.edge.controller.ess.ripplecontrolreceiver.powerproduction; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @ObjectClassDefinition(// - name = "Controller Ess Ripple Control Receiver", // + name = "Controller Ess Ripple Control Receiver, FNN-2bit", // description = "Controller to optimize energy distribution during peak hours by reducing the inverter output to 0, 30 or 60 percent.") @interface Config { @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") - String id() default "ctrlEssRippleControlReceiver0"; + String id() default "limiter0"; @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") String alias() default ""; diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/RelaisSignalPowerProductionLimiter.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/RelaisSignalPowerProductionLimiter.java new file mode 100644 index 00000000000..d34d268dec7 --- /dev/null +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/RelaisSignalPowerProductionLimiter.java @@ -0,0 +1,41 @@ +package io.openems.edge.controller.ess.ripplecontrolreceiver.powerproduction; + +import io.openems.common.channel.Level; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.controller.ess.ripplecontrolreceiver.EssRestrictionLevel; +import io.openems.edge.controller.ess.ripplecontrolreceiver.PowerProductionLimiterComponent; + +import static io.openems.common.channel.PersistencePriority.HIGH; + +public interface RelaisSignalPowerProductionLimiter extends PowerProductionLimiterComponent { + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + RESTRICTION_MODE(Doc.of(EssRestrictionLevel.values())// + .persistencePriority(HIGH)), // + + UPDATE_FAILURE(Doc.of(Level.WARNING)); + + private final Doc doc; + + ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + /** + * Represents the current {@link EssRestrictionLevel}. + * + *

+ * This is determined by the external ripple control signals. + *

+ * + * @return the current restriction level. + */ + public default EssRestrictionLevel getRestrictionLevel() { + return this.channel(ChannelId.RESTRICTION_MODE).value().asEnum(); + } +} diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/ControllerEssRippleControlReceiverImpl.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/RelaisSignalPowerProductionLimiterImpl.java similarity index 51% rename from io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/ControllerEssRippleControlReceiverImpl.java rename to io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/RelaisSignalPowerProductionLimiterImpl.java index 76a47c70bb7..717ff92ab6e 100644 --- a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/ControllerEssRippleControlReceiverImpl.java +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/powerproduction/RelaisSignalPowerProductionLimiterImpl.java @@ -1,9 +1,22 @@ -package io.openems.edge.controller.ess.ripplecontrolreceiver; - -import static io.openems.edge.controller.ess.ripplecontrolreceiver.EssRestrictionLevel.NO_RESTRICTION; - -import java.util.OptionalInt; +package io.openems.edge.controller.ess.ripplecontrolreceiver.powerproduction; +import io.openems.common.exceptions.OpenemsError; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.channel.BooleanReadChannel; +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.event.EdgeEventConstants; +import io.openems.edge.common.meta.GridFeedInLimitationType; +import io.openems.edge.common.sum.GridMode; +import io.openems.edge.common.sum.Sum; +import io.openems.edge.controller.ess.ripplecontrolreceiver.EssRestrictionLevel; +import io.openems.edge.controller.ess.ripplecontrolreceiver.PowerProductionLimiter; +import io.openems.edge.controller.ess.ripplecontrolreceiver.PowerProductionLimiterComponent; +import io.openems.edge.timedata.api.Timedata; +import io.openems.edge.timedata.api.TimedataProvider; +import io.openems.edge.timedata.api.utils.CalculateActiveTime; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -15,22 +28,15 @@ 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.event.Event; +import org.osgi.service.event.EventHandler; +import org.osgi.service.event.propertytypes.EventTopics; 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; -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.meta.GridFeedInLimitationType; -import io.openems.edge.common.sum.GridMode; -import io.openems.edge.common.sum.Sum; -import io.openems.edge.controller.api.Controller; -import io.openems.edge.timedata.api.Timedata; -import io.openems.edge.timedata.api.TimedataProvider; -import io.openems.edge.timedata.api.utils.CalculateActiveTime; +import java.util.OptionalInt; + +import static io.openems.edge.common.channel.ChannelUtils.setValue; +import static io.openems.edge.common.event.EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE; @Designate(ocd = Config.class, factory = true) @Component(// @@ -38,8 +44,11 @@ immediate = true, // configurationPolicy = ConfigurationPolicy.REQUIRE // ) -public class ControllerEssRippleControlReceiverImpl extends AbstractOpenemsComponent implements // - ControllerEssRippleControlReceiver, Controller, OpenemsComponent, TimedataProvider { +@EventTopics({ // + TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // +}) +public class RelaisSignalPowerProductionLimiterImpl extends AbstractOpenemsComponent implements RelaisSignalPowerProductionLimiter, + PowerProductionLimiterComponent, PowerProductionLimiter, OpenemsComponent, TimedataProvider, EventHandler { @Reference private Sum sum; @@ -47,36 +56,34 @@ public class ControllerEssRippleControlReceiverImpl extends AbstractOpenemsCompo @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) private volatile Timedata timedata = null; - @Reference - private ConfigurationAdmin cm; - @Reference private ComponentManager componentManager; - private EssRestrictionLevel currentRestriction = NO_RESTRICTION; private ChannelAddress zeroPercentChannelAddress; private ChannelAddress thirtyPercentChannelAddress; private ChannelAddress sixtyPercentChannelAddress; + private Integer maxNominalProductionPowerInW; + private final CalculateActiveTime cumulatedRestrictionTime = new CalculateActiveTime(this, - ControllerEssRippleControlReceiver.ChannelId.CUMULATED_RESTRICTION_TIME); + PowerProductionLimiterComponent.ChannelId.CUMULATED_RESTRICTION_TIME); - public ControllerEssRippleControlReceiverImpl() { + public RelaisSignalPowerProductionLimiterImpl() { super(// OpenemsComponent.ChannelId.values(), // - Controller.ChannelId.values(), // - ControllerEssRippleControlReceiver.ChannelId.values() // + PowerProductionLimiterComponent.ChannelId.values(), // + RelaisSignalPowerProductionLimiter.ChannelId.values() // ); } @Activate - private void activate(ComponentContext context, Config config) throws OpenemsNamedException { + private void activate(ComponentContext context, Config config) throws OpenemsError.OpenemsNamedException { super.activate(context, config.id(), config.alias(), config.enabled()); this.applyConfig(config); } @Modified - protected void modified(ComponentContext context, Config config) throws OpenemsNamedException { + protected void modified(ComponentContext context, Config config) throws OpenemsError.OpenemsNamedException { super.modified(context, config.id(), config.alias(), config.enabled()); // this.applyConfig(config); } @@ -87,39 +94,54 @@ protected void deactivate() { super.deactivate(); } - @Override - public void run() throws OpenemsNamedException { - var currentRestriction = NO_RESTRICTION; - if (this.sum.getGridMode().equals(GridMode.ON_GRID)) { - currentRestriction = EssRestrictionLevel.getRestrictionLevelByPriority(// - this.isRestrictionActive(this.zeroPercentChannelAddress), // - this.isRestrictionActive(this.thirtyPercentChannelAddress), // - this.isRestrictionActive(this.sixtyPercentChannelAddress)); - } + private void applyConfig(Config config) throws OpenemsError.OpenemsNamedException { + this.zeroPercentChannelAddress = ChannelAddress.fromString(config.inputChannelAddress1()); + this.thirtyPercentChannelAddress = ChannelAddress.fromString(config.inputChannelAddress2()); + this.sixtyPercentChannelAddress = ChannelAddress.fromString(config.inputChannelAddress3()); + } - switch (currentRestriction) { - case NO_RESTRICTION -> { - this.cumulatedRestrictionTime.update(false); - } - case ZERO_PERCENT, THIRTY_PERCENT, SIXTY_PERCENT -> { - // This may split into different cumulated times for each restriction level - this.cumulatedRestrictionTime.update(true); - } + protected void run() { + try { + if (this.sum.getGridMode() != GridMode.ON_GRID) { + this.setRestriction(EssRestrictionLevel.NO_RESTRICTION); + return; + } + + this.setRestriction(this.calculateRestrictionByRelayPorts()); + setValue(this, RelaisSignalPowerProductionLimiter.ChannelId.UPDATE_FAILURE, false); + } catch (Exception ex) { + // TODO + setValue(this, RelaisSignalPowerProductionLimiter.ChannelId.UPDATE_FAILURE, true); } + } - this.currentRestriction = currentRestriction; - this._setRestrictionMode(currentRestriction); + protected EssRestrictionLevel calculateRestrictionByRelayPorts() throws OpenemsError.OpenemsNamedException { + return EssRestrictionLevel.getRestrictionLevelByPriority(// + this.isRelayPortActive(this.zeroPercentChannelAddress), // + this.isRelayPortActive(this.thirtyPercentChannelAddress), // + this.isRelayPortActive(this.sixtyPercentChannelAddress)); } - protected boolean isRestrictionActive(ChannelAddress inputChannelAddress) throws OpenemsNamedException { + private boolean isRelayPortActive(ChannelAddress inputChannelAddress) throws OpenemsError.OpenemsNamedException { BooleanReadChannel inputChannel = this.componentManager.getChannel(inputChannelAddress); // 0/1 is reversed on relays board return !inputChannel.value().orElse(true); } + protected void setRestriction(EssRestrictionLevel restrictionLevel) { + setValue(this, RelaisSignalPowerProductionLimiter.ChannelId.RESTRICTION_MODE, restrictionLevel); + if (restrictionLevel == EssRestrictionLevel.NO_RESTRICTION) { + this.cumulatedRestrictionTime.update(false); + setValue(this, PowerProductionLimiterComponent.ChannelId.RESTRICTION, null); + } else { + this.cumulatedRestrictionTime.update(true); + setValue(this, PowerProductionLimiterComponent.ChannelId.RESTRICTION, restrictionLevel.getLimitationFactor() * this.maxNominalProductionPowerInW); + } + } + @Override - public EssRestrictionLevel essRestrictionLevel() { - return this.currentRestriction; + public String debugLog() { + return "Current limitation: " + this.getRestrictionLevel().getName(); } @Override @@ -127,35 +149,42 @@ public Timedata getTimedata() { return this.timedata; } - private void applyConfig(Config config) throws OpenemsNamedException { - this.zeroPercentChannelAddress = ChannelAddress.fromString(config.inputChannelAddress1()); - this.thirtyPercentChannelAddress = ChannelAddress.fromString(config.inputChannelAddress2()); - this.sixtyPercentChannelAddress = ChannelAddress.fromString(config.inputChannelAddress3()); + @Override + public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } + + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: + this.run(); + break; + } } @Override - public String debugLog() { - return "Current limitation: " + this.getRestrictionMode().getName(); + public void setMaxNominalProductionPower(int maxNominalProductionPowerInW) { + this.maxNominalProductionPowerInW = maxNominalProductionPowerInW; } /** * Calculates the feed-in limit from the meta component's grid feed-in * limitation. - * + * * @param type the type of grid feed-in limitation * @param limit the limit value (in W) if type is DYNAMIC_LIMITATION * @return the dynamic feed-in limit as OptionalInt */ public static OptionalInt feedInLimitFromMetaLimits(GridFeedInLimitationType type, Value limit) { return switch (type) { - case DYNAMIC_LIMITATION -> { - if (limit.isDefined()) { - yield OptionalInt.of(limit.get()); + case DYNAMIC_LIMITATION -> { + if (limit.isDefined()) { + yield OptionalInt.of(limit.get()); + } + yield OptionalInt.empty(); } - yield OptionalInt.empty(); - } - case NO_LIMITATION -> OptionalInt.empty(); - case UNDEFINED -> OptionalInt.empty(); + case NO_LIMITATION -> OptionalInt.empty(); + case UNDEFINED -> OptionalInt.empty(); }; } -} \ No newline at end of file +} diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/test/DummyPowerProductionLimiter.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/test/DummyPowerProductionLimiter.java new file mode 100644 index 00000000000..8a79091627b --- /dev/null +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/test/DummyPowerProductionLimiter.java @@ -0,0 +1,55 @@ +package io.openems.edge.controller.ess.ripplecontrolreceiver.test; + +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.test.AbstractDummyOpenemsComponent; +import io.openems.edge.controller.ess.ripplecontrolreceiver.EssRestrictionLevel; +import io.openems.edge.controller.ess.ripplecontrolreceiver.PowerProductionLimiterComponent; + +import java.util.function.Supplier; + +public class DummyPowerProductionLimiter extends AbstractDummyOpenemsComponent + implements PowerProductionLimiterComponent { + + private int maxNominalProductionPowerInW; + private Supplier gridFeedInLimitFunc; + + public DummyPowerProductionLimiter(String id) { + super(id, // + OpenemsComponent.ChannelId.values(), // + PowerProductionLimiterComponent.ChannelId.values() // + ); + } + + @Override + protected DummyPowerProductionLimiter self() { + return this; + } + + @Override + public void setMaxNominalProductionPower(int maxNominalProductionPowerInW) { + this.maxNominalProductionPowerInW = maxNominalProductionPowerInW; + } + + public DummyPowerProductionLimiter withGridFeedInLimit(Integer gridFeedInLimit) { + if (gridFeedInLimit == null) { + this.gridFeedInLimitFunc = null; + } else { + this.gridFeedInLimitFunc = () -> gridFeedInLimit; + } + return this.self(); + } + + public DummyPowerProductionLimiter withRestrictionLevel(EssRestrictionLevel restrictionLevel) { + if (restrictionLevel == null || restrictionLevel == EssRestrictionLevel.NO_RESTRICTION) { + this.gridFeedInLimitFunc = null; + } else { + this.gridFeedInLimitFunc = () -> (int) (this.maxNominalProductionPowerInW * restrictionLevel.getLimitationFactor()); + } + return this.self(); + } + + @Override + public Integer getGridFeedInLimit() { + return this.gridFeedInLimitFunc.get(); + } +} diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/test/DummyRippleControlReceiver.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/test/DummyRippleControlReceiver.java deleted file mode 100644 index dbce9f8ddae..00000000000 --- a/io.openems.edge.controller.ess.ripplecontrolreceiver/src/io/openems/edge/controller/ess/ripplecontrolreceiver/test/DummyRippleControlReceiver.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.openems.edge.controller.ess.ripplecontrolreceiver.test; - -import io.openems.edge.common.component.OpenemsComponent; -import io.openems.edge.common.test.AbstractDummyOpenemsComponent; -import io.openems.edge.controller.api.Controller; -import io.openems.edge.controller.ess.ripplecontrolreceiver.ControllerEssRippleControlReceiver; -import io.openems.edge.controller.ess.ripplecontrolreceiver.EssRestrictionLevel; - -/** - * Dummy implementation of {@link ControllerEssRippleControlReceiver} for testing. - */ -public class DummyRippleControlReceiver extends AbstractDummyOpenemsComponent - implements ControllerEssRippleControlReceiver { - - private EssRestrictionLevel restrictionLevel = EssRestrictionLevel.NO_RESTRICTION; - - public DummyRippleControlReceiver(String id) { - super(id, // - OpenemsComponent.ChannelId.values(), // - Controller.ChannelId.values(), // - ControllerEssRippleControlReceiver.ChannelId.values()); - } - - @Override - protected DummyRippleControlReceiver self() { - return this; - } - - @Override - public void run() { - // Dummy implementation - does nothing - } - - @Override - public EssRestrictionLevel essRestrictionLevel() { - return this.restrictionLevel; - } - - /** - * Sets the restriction level for this dummy instance. - * - * @param restrictionLevel the {@link EssRestrictionLevel} to set - * @return myself for method chaining - */ - public DummyRippleControlReceiver withRestrictionLevel(EssRestrictionLevel restrictionLevel) { - this.restrictionLevel = restrictionLevel; - return this.self(); - } -} - diff --git a/io.openems.edge.controller.ess.ripplecontrolreceiver/test/io/openems/edge/controller/ess/ripplecontrolreceiver/MyConfig.java b/io.openems.edge.controller.ess.ripplecontrolreceiver/test/io/openems/edge/controller/ess/ripplecontrolreceiver/MyConfig.java index 12fafcb152b..beb9d0a826d 100644 --- a/io.openems.edge.controller.ess.ripplecontrolreceiver/test/io/openems/edge/controller/ess/ripplecontrolreceiver/MyConfig.java +++ b/io.openems.edge.controller.ess.ripplecontrolreceiver/test/io/openems/edge/controller/ess/ripplecontrolreceiver/MyConfig.java @@ -1,6 +1,7 @@ package io.openems.edge.controller.ess.ripplecontrolreceiver; import io.openems.common.test.AbstractComponentConfig; +import io.openems.edge.controller.ess.ripplecontrolreceiver.powerproduction.Config; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { diff --git a/io.openems.edge.core/bnd.bnd b/io.openems.edge.core/bnd.bnd index 75bec006783..9f9e1af8b1c 100644 --- a/io.openems.edge.core/bnd.bnd +++ b/io.openems.edge.core/bnd.bnd @@ -8,6 +8,7 @@ Bundle-Version: 1.0.0.${tstamp} com.fazecast.jSerialComm,\ com.squareup.okhttp3,\ io.openems.common,\ + io.openems.common.bridge.eebus,\ io.openems.common.bridge.http,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ 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 184916e0660..5138d383582 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 @@ -197,7 +197,7 @@ private Prediction getPredictionSum(Sum.ChannelId channelId) { PRODUCTION_TO_CONSUMPTION_POWER, PRODUCTION_TO_CONSUMPTION_ENERGY, PRODUCTION_TO_ESS_POWER, PRODUCTION_TO_ESS_ENERGY, PRODUCTION_TO_GRID_POWER, PRODUCTION_TO_GRID_ENERGY, - HAS_IGNORED_COMPONENT_STATES -> + HAS_IGNORED_COMPONENT_STATES, BATTERY_INVERTER_MAX_APPARENT_POWER -> EMPTY_PREDICTION; case UNMANAGED_CONSUMPTION_ACTIVE_POWER -> 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 da3f8b6dbb5..d5ab2897bb4 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 @@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; +import io.openems.edge.controller.ess.ripplecontrolreceiver.PowerProductionLimiter; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -75,7 +76,6 @@ import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.common.type.TypeUtils; import io.openems.edge.common.update.Updateable; -import io.openems.edge.controller.ess.ripplecontrolreceiver.ControllerEssRippleControlReceiver; import io.openems.edge.ess.power.api.Power; import io.openems.edge.goodwe.batteryinverter.statemachine.Context; import io.openems.edge.goodwe.batteryinverter.statemachine.StateMachine; @@ -153,7 +153,7 @@ public class GoodWeBatteryInverterImpl extends AbstractGoodWe implements GoodWeB private Meta meta; @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) - protected volatile ControllerEssRippleControlReceiver rcr; + protected volatile PowerProductionLimiter powerProductionLimiter; @Override @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) @@ -785,9 +785,11 @@ private void handleGridFeed(Config config, GridFeedInLimitationType limitType) t } // Limit from Ripple Control Receiver (Minimum of both limits) - if (this.rcr != null && this.rcr.isEnabled()) { + if (this.powerProductionLimiter != null) { + this.powerProductionLimiter.setMaxNominalProductionPower(maxApparentPower); + enableFeedInLimit = true; - gridFeedInLimit = Math.min(gridFeedInLimit, this.rcr.getDynamicGridFeedInLimit(maxApparentPower)); + gridFeedInLimit = Math.min(gridFeedInLimit, this.powerProductionLimiter.getGridFeedInLimit()); } this.handleFeedInSetting(enableFeedInLimit, gridFeedInLimit, this.getGoodweType());