diff --git a/io.openems.edge.bridge.modbus/bnd.bnd b/io.openems.edge.bridge.modbus/bnd.bnd
index 762ccd2b3d9..ef6b4a48ecf 100644
--- a/io.openems.edge.bridge.modbus/bnd.bnd
+++ b/io.openems.edge.bridge.modbus/bnd.bnd
@@ -12,6 +12,6 @@ Bundle-Version: 1.0.0.${tstamp}
io.openems.edge.meter.api,\
io.openems.edge.pvinverter.api,\
io.openems.j2mod,\
-
+ io.openems.edge.ess.api
-testpath: \
${testpath}
diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/FilteredSunSpecModel.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/FilteredSunSpecModel.java
new file mode 100644
index 00000000000..37451a0d75c
--- /dev/null
+++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/FilteredSunSpecModel.java
@@ -0,0 +1,56 @@
+package io.openems.edge.bridge.modbus.sunspec;
+
+import java.util.Arrays;
+
+/**
+ * Represents a Filtered SunSpec Model.
+ */
+public class FilteredSunSpecModel implements SunSpecModel {
+
+ private final SunSpecModel delegate;
+ private final SunSpecPoint[] points;
+
+ public FilteredSunSpecModel(SunSpecModel delegate, SunSpecPoint[] points) {
+ this.delegate = delegate;
+ this.points = points;
+ }
+
+ @Override
+ public String name() {
+ return this.delegate.name();
+ }
+
+ @Override
+ public String label() {
+ return this.delegate.label();
+ }
+
+ @Override
+ public SunSpecPoint[] points() {
+ return this.points;
+ }
+
+ /**
+ * Creates a {@link FilteredSunSpecModel} that delegates to the given
+ * {@link SunSpecModel} but excludes the specified {@link SunSpecPoint}s
+ * from the returned {@link SunSpecModel#points()}.
+ *
+ *
+ * This can be used if a device implements a standard SunSpec model but
+ * does not support some points (e.g. vendor-specific event registers).
+ * The returned model behaves like the original model except that the
+ * excluded points are not exposed and therefore not mapped to Modbus
+ * registers.
+ *
+ * @param delegate the original {@link SunSpecModel}
+ * @param excluded the {@link SunSpecPoint}s that should be removed
+ * @return a {@link FilteredSunSpecModel} without the specified points
+ */
+ public static FilteredSunSpecModel withoutPoints(SunSpecModel delegate, SunSpecPoint... excluded) {
+ var excludedSet = Arrays.asList(excluded);
+ var filtered = Arrays.stream(delegate.points())
+ .filter(p -> !excludedSet.contains(p))
+ .toArray(SunSpecPoint[]::new);
+ return new FilteredSunSpecModel(delegate, filtered);
+ }
+}
\ No newline at end of file
diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/ess/AbstractSunSpecEss.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/ess/AbstractSunSpecEss.java
new file mode 100644
index 00000000000..7754ca51e30
--- /dev/null
+++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/ess/AbstractSunSpecEss.java
@@ -0,0 +1,97 @@
+package io.openems.edge.bridge.modbus.sunspec.ess;
+
+import java.util.Map;
+import java.util.Optional;
+
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.openems.common.exceptions.OpenemsException;
+import io.openems.edge.bridge.modbus.sunspec.AbstractOpenemsSunSpecComponent;
+import io.openems.edge.bridge.modbus.sunspec.SunSpecModel;
+import io.openems.edge.bridge.modbus.sunspec.SunSpecPoint;
+import io.openems.edge.common.channel.Channel;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.common.sum.GridMode;
+import io.openems.edge.common.taskmanager.Priority;
+import io.openems.edge.ess.api.ManagedSymmetricEss;
+import io.openems.edge.ess.api.SymmetricEss;
+
+public abstract class AbstractSunSpecEss extends AbstractOpenemsSunSpecComponent
+ implements ManagedSymmetricEss, SymmetricEss, OpenemsComponent {
+
+ private final Logger log = LoggerFactory.getLogger(AbstractSunSpecEss.class);
+
+ public AbstractSunSpecEss(Map activeModels,
+ io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds,
+ io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) throws OpenemsException {
+ super(activeModels, firstInitialChannelIds, furtherInitialChannelIds);
+ this._setGridMode(GridMode.ON_GRID);
+ }
+
+ /**
+ * Make sure to call this method from the inheriting OSGi Component.
+ *
+ * @param context ComponentContext of this component. Receive it
+ * from parameter for @Activate
+ * @param id ID of this component. Typically 'config.id()'
+ * @param alias Human-readable name of this Component. Typically
+ * 'config.alias()'. Defaults to 'id' if empty
+ * @param enabled Whether the component should be enabled.
+ * Typically 'config.enabled()'
+ * @param unitId Unit-ID of the Modbus target
+ * @param cm An instance of ConfigurationAdmin. Receive it
+ * using @Reference
+ * @param modbusReference The name of the @Reference setter method for the
+ * Modbus bridge - e.g. 'Modbus' if you have a
+ * setModbus()-method
+ * @param modbusId The ID of the Modbus bridge. Typically
+ * 'config.modbus_id()'
+ * @param readFromCommonBlockNo the starting block number
+ * @return true if the target filter was updated. You may use it to abort the
+ * activate() method.
+ * @throws OpenemsException on error
+ */
+ @Override
+ protected boolean activate(ComponentContext context, String id, String alias, boolean enabled, int unitId,
+ ConfigurationAdmin cm, String modbusReference, String modbusId, int readFromCommonBlockNo)
+ throws OpenemsException {
+ return super.activate(context, id, alias, enabled, unitId, cm, modbusReference, modbusId,
+ readFromCommonBlockNo);
+ }
+
+ /**
+ * Make sure to call this method from the inheriting OSGi Component.
+ */
+ @Override
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ public String debugLog() {
+ return new StringBuilder() //
+ .append("SoC:").append(this.getSoc().asString()) //
+ .append("|ESS ActivePower:").append(this.getActivePower().asString()) //
+ .toString();
+ }
+
+ @Override
+ protected void onSunSpecInitializationCompleted() {
+ this.logInfo(this.log, "SunSpec initialization finished. " + this.channels().size() + " Channels available.");
+ }
+
+ @Override
+ protected > Optional getSunSpecChannel(SunSpecPoint point) {
+ return super.getSunSpecChannel(point);
+ }
+
+ @Override
+ protected > T getSunSpecChannelOrError(SunSpecPoint point) throws OpenemsException {
+ return super.getSunSpecChannelOrError(point);
+ }
+}
\ No newline at end of file
diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/ess/package-info.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/ess/package-info.java
new file mode 100644
index 00000000000..d821ba0579c
--- /dev/null
+++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/ess/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.modbus.sunspec.ess;
diff --git a/io.openems.edge.solaredge/bnd.bnd b/io.openems.edge.solaredge/bnd.bnd
index e39b69e164c..debd4597726 100644
--- a/io.openems.edge.solaredge/bnd.bnd
+++ b/io.openems.edge.solaredge/bnd.bnd
@@ -6,11 +6,16 @@ Bundle-Version: 1.0.0.${tstamp}
-buildpath: \
${buildpath},\
io.openems.common,\
+ io.openems.edge.battery.api,\
+ io.openems.edge.batteryinverter.api,\
io.openems.edge.bridge.modbus,\
io.openems.edge.common,\
+ io.openems.edge.ess.api,\
+ io.openems.edge.ess.generic,\
io.openems.edge.meter.api,\
io.openems.edge.pvinverter.api,\
-
+ io.openems.edge.timedata.api,\
+ io.openems.j2mod
-testpath: \
${testpath},\
io.openems.j2mod,\
diff --git a/io.openems.edge.solaredge/readme.adoc b/io.openems.edge.solaredge/readme.adoc
index 78a9e34302a..b09f1b8b5aa 100644
--- a/io.openems.edge.solaredge/readme.adoc
+++ b/io.openems.edge.solaredge/readme.adoc
@@ -1,8 +1,19 @@
-= SolarEdge PV Inverter + Grid-Meter
+= SolarEdge PV Inverter + Grid-Meter + Hybrid Inverters
-Implementation of the SolarEdge PV inverters.
-
-Implemented Natures:
+PV-Inverter::
- ElectricityMeter
+ESS::
+- HybridEss
+- SymmetricEss
+- ManagedSymmetricEss
+- AsymmetricEss
+- ManagedAsymmetricEss
+- SinglePhaseEss
+- ManagedSinglePhaseEss
+
+Charger::
+- EssDcCharger
+
+
https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.solaredge[Source Code icon:github[]]
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/charger/Config.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/charger/Config.java
new file mode 100644
index 00000000000..5f321b6ab1c
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/charger/Config.java
@@ -0,0 +1,28 @@
+package io.openems.edge.solaredge.charger;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+
+@ObjectClassDefinition(name = "SolarEdge Charger", //
+ description = "Implements the SolarEdge Charger.")
+@interface Config {
+
+ @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
+ String id() default "charger0";
+
+ @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 = "SolarEdge ESS-Inverter", description = "ID of SolarEdge Energy Storage System.")
+ String essInverter_id() default "ess0";
+
+ @AttributeDefinition(name = "SolarEdge ESS-Inverter target filter", description = "This is auto-generated by 'SolarEdge ESS-Inverter'.")
+ String essInverter_target() default "(enabled=true)";
+
+ String webconsole_configurationFactory_nameHint() default "Charger SolarEdge [{id}]";
+
+}
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/charger/SolarEdgeCharger.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/charger/SolarEdgeCharger.java
new file mode 100644
index 00000000000..53efa123e75
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/charger/SolarEdgeCharger.java
@@ -0,0 +1,24 @@
+package io.openems.edge.solaredge.charger;
+
+import io.openems.edge.common.channel.Doc;
+
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.ess.dccharger.api.EssDcCharger;
+
+public interface SolarEdgeCharger extends EssDcCharger, OpenemsComponent {
+
+ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
+ ;
+ private final Doc doc;
+
+ private ChannelId(Doc doc) {
+ this.doc = doc;
+ }
+
+ @Override
+ public Doc doc() {
+ return this.doc;
+ }
+ }
+
+}
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/charger/SolarEdgeChargerImpl.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/charger/SolarEdgeChargerImpl.java
new file mode 100644
index 00000000000..d0d6871caa2
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/charger/SolarEdgeChargerImpl.java
@@ -0,0 +1,151 @@
+package io.openems.edge.solaredge.charger;
+
+import java.util.function.Consumer;
+
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+import org.osgi.service.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.channel.AccessMode;
+import io.openems.common.exceptions.OpenemsException;
+import io.openems.edge.common.channel.IntegerReadChannel;
+import io.openems.edge.common.channel.value.Value;
+import io.openems.edge.common.component.AbstractOpenemsComponent;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.common.event.EdgeEventConstants;
+import io.openems.edge.common.modbusslave.ModbusSlave;
+import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable;
+import io.openems.edge.common.modbusslave.ModbusSlaveTable;
+import io.openems.edge.ess.dccharger.api.EssDcCharger;
+import io.openems.edge.solaredge.ess.SolarEdgeEss;
+import io.openems.edge.timedata.api.Timedata;
+import io.openems.edge.timedata.api.TimedataProvider;
+
+@Designate(ocd = Config.class, factory = true)
+@Component(//
+ name = "SolarEdge.ESS.Charger", //
+ immediate = true, //
+ configurationPolicy = ConfigurationPolicy.REQUIRE //
+)
+@EventTopics({ //
+ EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE //
+})
+public class SolarEdgeChargerImpl extends AbstractOpenemsComponent
+ implements SolarEdgeCharger, EssDcCharger, OpenemsComponent, EventHandler, TimedataProvider, ModbusSlave {
+
+ private SolarEdgeListener voltageListener;
+
+ @Reference
+ private ConfigurationAdmin cm;
+
+ @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY)
+ private SolarEdgeEss essInverter;
+
+ @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL)
+ private volatile Timedata timedata;
+
+ public SolarEdgeChargerImpl() throws OpenemsException {
+ super(//
+ OpenemsComponent.ChannelId.values(), //
+ EssDcCharger.ChannelId.values(), //
+ SolarEdgeCharger.ChannelId.values() //
+ );
+ }
+
+ @Activate
+ private void activate(ComponentContext context, Config config) throws OpenemsException {
+ super.activate(context, config.id(), config.alias(), config.enabled());
+
+ this.voltageListener = new SolarEdgeListener(this, this.essInverter, SolarEdgeEss.ChannelId.VOLTAGE_DC,
+ EssDcCharger.ChannelId.VOLTAGE);
+
+ this.essInverter.addCharger(this);
+
+ if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "essInverter",
+ config.essInverter_id())) {
+ return;
+ }
+ }
+
+ @Override
+ @Deactivate
+ protected void deactivate() {
+ this.essInverter.removeCharger(this);
+ this.voltageListener.deactivate();
+ super.deactivate();
+ }
+
+
+ @Override
+ public String debugLog() {
+ return "L:" + this.getActualPower().asString();
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ if (!this.isEnabled()) {
+ return;
+ }
+
+ switch (event.getTopic()) {
+ case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE -> {
+ this.updateActualEnergy();
+ }
+ }
+ }
+
+ /**
+ * Update the Energy values using data from SolarEdgeEssChannel.
+ */
+ private void updateActualEnergy() {
+ this._setActualEnergy(this.essInverter.getActiveProductionEnergy().get());
+ }
+
+ @Override
+ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) {
+ return new ModbusSlaveTable(//
+ OpenemsComponent.getModbusSlaveNatureTable(accessMode), //
+ EssDcCharger.getModbusSlaveNatureTable(accessMode), //
+ ModbusSlaveNatureTable.of(SolarEdgeCharger.class, accessMode, 100) //
+ .build());
+ }
+
+ @Override
+ public Timedata getTimedata() {
+ return this.timedata;
+ }
+
+ private static class SolarEdgeListener implements Consumer> {
+
+ private final IntegerReadChannel solarEdgeChannel;
+ private final IntegerReadChannel mirrorChannel;
+
+ public SolarEdgeListener(SolarEdgeChargerImpl parent, SolarEdgeEss essInverter,
+ io.openems.edge.common.channel.ChannelId solarEdgeChannel, io.openems.edge.common.channel.ChannelId mirrorChannel) {
+ this.solarEdgeChannel = essInverter.channel(solarEdgeChannel);
+ this.mirrorChannel = parent.channel(mirrorChannel);
+ this.solarEdgeChannel.onSetNextValue(this);
+ }
+
+ public void deactivate() {
+ this.solarEdgeChannel.removeOnSetNextValueCallback(this);
+ }
+
+ @Override
+ public void accept(Value t) {
+ this.mirrorChannel.setNextValue(t);
+ }
+ }
+
+}
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/AcChargePolicy.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/AcChargePolicy.java
new file mode 100644
index 00000000000..6c36394c98d
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/AcChargePolicy.java
@@ -0,0 +1,35 @@
+package io.openems.edge.solaredge.enums;
+
+import io.openems.common.types.OptionsEnum;
+
+public enum AcChargePolicy implements OptionsEnum {
+ UNDEFINED(-1, "Undefined"), //
+ DISABLED(0, "Disabled"), //
+ ALWAYS_ALLOWED(1, "Always allowed"), // needed for AC coupling operation. Allows unlimited charging from the AC. When used with Maximize self-consumption, only excess power is used for charging (charging from the grid is not allowed).
+ FIXED_ENERGY_LIMIT(2, "Fixed Energy Limit"), // allows AC charging with a fixed yearly (Jan 1 to Dec 31) limit (needed for meeting ITC regulation in the US)
+ PERCENT_OF_PRODUCTION(3, "Percent of Production"); // allows AC charging with a % of system production year to date limit (needed for meeting ITC regulation in the US)
+
+ private final int value;
+ private final String name;
+
+ private AcChargePolicy(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 UNDEFINED;
+ }
+
+}
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/BatteryStatus.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/BatteryStatus.java
new file mode 100644
index 00000000000..f0cf267fa40
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/BatteryStatus.java
@@ -0,0 +1,40 @@
+package io.openems.edge.solaredge.enums;
+
+import io.openems.common.types.OptionsEnum;
+
+public enum BatteryStatus implements OptionsEnum {
+ UNDEFINED(-1, "Undefined"), //
+ SE_BATT_STATUS_OFF(0, "Off"), //
+ SE_BATT_STATUS_STBY(1, "Standby"), //
+ SE_BATT_STATUS_INIT(2, "Init"), //
+ SE_BATT_STATUS_CHARGE(3, "Charge"), //
+ SE_BATT_STATUS_DISCHARGE(4, "Discharge"), //
+ SE_BATT_STATUS_FAULT(5, "Fault"), //
+ // 6 doesn´t exist
+ SE_BATT_STATUS_IDLE(7, "Idle"); //
+
+
+ private final int value;
+ private final String name;
+
+ private BatteryStatus(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 UNDEFINED;
+ }
+}
+
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/CommandMode.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/CommandMode.java
new file mode 100644
index 00000000000..79634f01340
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/CommandMode.java
@@ -0,0 +1,117 @@
+package io.openems.edge.solaredge.enums;
+
+import io.openems.common.types.OptionsEnum;
+
+public enum CommandMode implements OptionsEnum {
+
+ UNDEFINED(-1, "Undefined"), //
+
+ /**
+ * Scenario: System shutdown.
+ *
+ *
+ * Stop working and turn to wait mode
+ */
+ STOPPED(0, "Stopped"), //
+
+ /**
+ * Scenario: Control the battery to charge PV excess only.
+ *
+ *
+ * Charge from PV first, before producing power to the AC.
+ *
+ *
+ * Only PV excess power not going to AC is used for charging the battery. Inverter NominalActivePowerLimit (or the
+ * inverter rated power whichever is lower) sets how much power the inverter is producing to the AC. In this mode,
+ * the battery cannot be discharged. If the PV power is lower than NominalActivePowerLimit the AC production will
+ * be equal to the PV power.
+ */
+ CHARGE_BAT_EXCESS(1, "Only PV excess power not going to AC is used for charging the battery. "),
+
+ /**
+ * Scenario: Control the battery to keep charging.
+ *
+ *
+ * Charge from PV first, before producing power to the AC.
+ *
+ *
+ * The Battery charge has higher priority than AC production. First charge the battery then produce AC.
+ * If StorageRemoteCtrl_ChargeLimit is lower than PV excess power goes to AC according to NominalActivePowerLimit.
+ * If NominalActivePowerLimit is reached and battery StorageRemoteCtrl_ChargeLimit is reached, PV power is curtailed.
+ */
+ CHARGE_BAT_FIRST(2, "Charge from PV first, before producing power to the AC."),
+
+ /**
+ * Scenario: Force the battery to work at set power value.
+ *
+ *
+ * Charge from PV+AC according to the max battery power
+ *
+ *
+ * Charge from both PV and AC with priority on PV power.
+ * If PV production is lower than StorageRemoteCtrl_ChargeLimit, the battery will be charged from AC up to
+ * NominalActivePow-erLimit. In this case AC power = StorageRemoteCtrl_ChargeLimit- PVpower.
+ * If PV power is larger than StorageRemoteCtrl_ChargeLimit the excess PV power will be directed to the AC up to the
+ * Nominal-ActivePowerLimit beyond which the PV is curtailed.
+ */
+ CHARGE_BAT(3, "Charge Bat"),
+
+ /**
+ * Scenario: Force the battery to work at set power value.
+ *
+ *
+ * Maximize export – discharge battery to meet max inverter AC limit.
+ *
+ *
+ * AC power is maintained to NominalActivePowerLimit, using PV power and/or battery power. If the PV power is not
+ * sufficient, battery power is used to complement AC power up to StorageRemoteCtrl_DishargeLimit. In this mode,
+ * charging excess power will occur if there is more PV than the AC limit.
+ */
+ DISCHARGE_BAT(4, "Discharge Bat"),
+
+ /**
+ * Scenario: Force the battery to balance grid meter to zero.
+ *
+ *
+ * Discharge to meet loads consumption. Discharging to the grid is not allowed. This mode requires installation
+ * of a SolarEdge Electricity Meter on the grid connection point.
+ */
+ DISCHARGE_BAT_WITH_BALANCE(5, "Discharge Bat+BalanceZero"),
+
+ /**
+ * Scenario: Force the battery to reduce electricity purchased from the grid.
+ *
+ *
+ * Maximize self-consumption.
+ *
+ *
+ * In this mode, the battery is automatically charged and discharged to meet consumption needs and reduce the amount
+ * of electricity purchased from the grid. This mode requires installation of a SolarEdge Electricity Meter, either
+ * on the grid connection point or on the load connection point.
+ */
+ AUTO(7, "Auto"); //
+
+ private final int value;
+ private final String name;
+
+ private CommandMode(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 UNDEFINED;
+ }
+}
+
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/ControlMode.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/ControlMode.java
new file mode 100644
index 00000000000..a91e0b91a33
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/ControlMode.java
@@ -0,0 +1,23 @@
+package io.openems.edge.solaredge.enums;
+
+public enum ControlMode {
+
+ /**
+ * Uses the internal 'AUTO' mode of the SolarEdge inverter. Allows no remote
+ * control of Set-Points. Requires a SolarEdge Smart Meter at the grid junction
+ * point.
+ */
+ INTERNAL,
+ /**
+ * Uses the internal 'AUTO' mode of the SolarEdge inverter but smartly switches to
+ * other modes if required. Requires a SolarEdge Smart Meter at the grid junction
+ * point.
+ */
+ SMART,
+ /**
+ * Full control of the SolarEdge inverter by OpenEMS. Slower than internal 'AUTO'
+ * mode, but does not require a SolarEdge Smart Meter at the grid junction point.
+ */
+ REMOTE;
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/MeterCommunicateStatus.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/MeterCommunicateStatus.java
new file mode 100644
index 00000000000..56da9fd0caa
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/MeterCommunicateStatus.java
@@ -0,0 +1,32 @@
+package io.openems.edge.solaredge.enums;
+
+import io.openems.common.types.OptionsEnum;
+
+public enum MeterCommunicateStatus implements OptionsEnum {
+ UNDEFINED(-1, "Undefined"), //
+ NO_METER(65535, "NO_METER"), //
+ OK(1, "OK"); //
+
+ private final int value;
+ private final String option;
+
+ private MeterCommunicateStatus(int value, String option) {
+ this.value = value;
+ this.option = option;
+ }
+
+ @Override
+ public int getValue() {
+ return this.value;
+ }
+
+ @Override
+ public String getName() {
+ return this.option;
+ }
+
+ @Override
+ public OptionsEnum getUndefined() {
+ return UNDEFINED;
+ }
+}
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/SeControlMode.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/SeControlMode.java
new file mode 100644
index 00000000000..12353b2a3d0
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/enums/SeControlMode.java
@@ -0,0 +1,35 @@
+package io.openems.edge.solaredge.enums;
+
+import io.openems.common.types.OptionsEnum;
+
+public enum SeControlMode implements OptionsEnum {
+ UNDEFINED(-1, "Undefined"), //
+ DISABLED(0, "Disabled"), //
+ MAX_SELF_CONSUMPTION(1, "Maximize Self Consumption"), // requires a SolarEdge Electricity meter on the grid or load connection point
+ TIME_OF_USE(2, "Time of Use (Profile programming)"), // requires a SolarEdge Electricity meter on the grid or load connection point
+ BACKUP_ONLY(3, "Backup Only"), // (applicable only for systems support backup functionality)
+ REMOTE_CONTROL(4, "Remote Control"); // the battery charge/discharge state is controlled by an external controller
+
+ private final int value;
+ private final String name;
+
+ private SeControlMode(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 UNDEFINED;
+ }
+}
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/AllowedChargeDischargeHandler.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/AllowedChargeDischargeHandler.java
new file mode 100644
index 00000000000..7f0c7381e1d
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/AllowedChargeDischargeHandler.java
@@ -0,0 +1,88 @@
+package io.openems.edge.solaredge.ess;
+
+import io.openems.edge.common.channel.IntegerReadChannel;
+import io.openems.edge.common.component.ClockProvider;
+import io.openems.edge.common.type.TypeUtils;
+import io.openems.edge.ess.generic.common.AbstractAllowedChargeDischargeHandler;
+import io.openems.edge.solaredge.charger.SolarEdgeCharger;
+import io.openems.edge.battery.api.Battery;
+import io.openems.edge.batteryinverter.api.SymmetricBatteryInverter;
+
+public class AllowedChargeDischargeHandler extends AbstractAllowedChargeDischargeHandler {
+
+ public AllowedChargeDischargeHandler(SolarEdgeEssImpl parent) {
+ super(parent);
+ }
+
+ @Override
+ public void accept(ClockProvider clockProvider, Battery battery, SymmetricBatteryInverter inverter) {
+ this.accept(clockProvider);
+ }
+
+ /**
+ * Calculates AllowedChargePower and AllowedDischargePower and sets the
+ * Channels.
+ *
+ * @param clockProvider a {@link ClockProvider}
+ */
+ public void accept(ClockProvider clockProvider) {
+ IntegerReadChannel bmsMaxChargePowerChannel = parent.channel(SolarEdgeEss.ChannelId.BATTERY1_MAX_CHARGE_CONTINUES_POWER);
+ IntegerReadChannel bmsMaxDischargePowerChannel = parent.channel(SolarEdgeEss.ChannelId.BATTERY1_MAX_DISCHARGE_CONTINUES_POWER);
+ var bmsPseudoVoltage = 1;
+ var bmsChargePseudoImax = bmsMaxChargePowerChannel.getNextValue().orElse(0) / bmsPseudoVoltage;
+ var bmsDischargePseudoImax = bmsMaxDischargePowerChannel.getNextValue().orElse(0) / bmsPseudoVoltage;
+ this.calculateAllowedChargeDischargePower(clockProvider, true, bmsChargePseudoImax, bmsDischargePseudoImax, bmsPseudoVoltage);
+
+ // Battery limits
+ var batteryAllowedChargePower = Math.round(this.lastBatteryAllowedChargePower);
+ var batteryAllowedDischargePower = Math.round(this.lastBatteryAllowedDischargePower);
+
+ // PV-Production
+ Integer pvProduction = 0;
+ for (SolarEdgeCharger charger : parent.chargers) {
+ pvProduction = TypeUtils.sum(pvProduction, charger.getActualPowerChannel().getNextValue().orElse(0));
+ }
+
+ // Block battery charging on battery full
+ if (parent.getSoc().orElse(100) >= 100) {
+ batteryAllowedChargePower = 0;
+ }
+
+ // Block battery discharging on battery empty
+ if (parent.getSoc().orElse(0) <= 10) {
+ batteryAllowedDischargePower = 0;
+ }
+
+ // Calculates Maximum Allowed AC-Charge Power as positive numbers (or negative when force discharge is active)
+ // Force discharge: pvProduction>batteryAllowedChargePower requires a minimum discharge
+ var acAllowedChargePower = batteryAllowedChargePower - pvProduction;
+
+ // Calculates Maximum Allowed AC-Discharge Power as positive numbers
+ var acAllowedDischargePower = TypeUtils.min(batteryAllowedDischargePower + pvProduction,parent.getMaxApparentPower().orElse(0));
+
+ // Inverter limits
+ var maxApparentPower = parent.getMaxApparentPower().orElse(0);
+
+ // Force Discharge active?
+ if (acAllowedChargePower < 0) {
+
+ // Limit forced DischargePower to maxApparentPower
+ if (Math.abs(acAllowedChargePower) > maxApparentPower) {
+ acAllowedChargePower = maxApparentPower * (-1);
+ }
+
+ // Make sure AllowedDischargePower is greater-or-equals absolute AllowedChargePower
+ acAllowedDischargePower = Math.max(Math.abs(acAllowedChargePower), acAllowedDischargePower);
+ } else {
+
+ // Limit acChargerPower to maxApparentPower
+ if (acAllowedChargePower > maxApparentPower) {
+ acAllowedChargePower = maxApparentPower;
+ }
+ }
+
+ // Apply AllowedChargePower and AllowedDischargePower
+ this.parent._setAllowedChargePower(acAllowedChargePower * -1 /* invert charge power */);
+ this.parent._setAllowedDischargePower(acAllowedDischargePower);
+ }
+}
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/ApplyPowerHandler.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/ApplyPowerHandler.java
new file mode 100644
index 00000000000..e60d331e219
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/ApplyPowerHandler.java
@@ -0,0 +1,277 @@
+package io.openems.edge.solaredge.ess;
+
+import static java.lang.Math.round;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.edge.common.channel.EnumReadChannel;
+import io.openems.edge.common.channel.EnumWriteChannel;
+import io.openems.edge.common.channel.IntegerWriteChannel;
+import io.openems.edge.common.channel.value.Value;
+import io.openems.edge.common.type.TypeUtils;
+import io.openems.edge.solaredge.enums.AcChargePolicy;
+import io.openems.edge.solaredge.enums.CommandMode;
+import io.openems.edge.solaredge.enums.ControlMode;
+import io.openems.edge.solaredge.enums.MeterCommunicateStatus;
+import io.openems.edge.solaredge.enums.SeControlMode;
+
+public class ApplyPowerHandler {
+
+ static final float DISCHARGE_EFFICIENCY_FACTOR = 0.95F;
+
+ /**
+ * Apply the desired Active-Power Set-Point by setting the appropriate
+ * REMOTE_CONTROL_COMMAND_MODE and CHARGE/DISCHARGE_LIMIT settings.
+ *
+ * @param solarEdge the SolarEdge ESS
+ * @param setActivePower the Active-Power Set-Point
+ * @param controlMode the {@link ControlMode} to handle the different
+ * {@link CommandMode} for the solarEdge battery inverter
+ * @param gridActivePower the grid active power
+ * @param essActivePower the ESS active power
+ * @param isPidEnabled if PID Filter is enabled
+ * @throws OpenemsNamedException on error
+ */
+ public synchronized void apply(SolarEdgeEss solarEdge, int setActivePower, ControlMode controlMode,
+ Value gridActivePower, Value essActivePower, boolean isPidEnabled) throws OpenemsNamedException {
+
+ // Update Warn Channels
+ this.checkControlModeRequiresRemoteControl(solarEdge, controlMode);
+ this.checkControlModeRequiresAcCharge(solarEdge, controlMode);
+ this.checkControlModeWithActivePid(solarEdge, controlMode, isPidEnabled);
+ this.checkControlModeRequiresSmartMeter(solarEdge, controlMode);
+
+ // get pv production
+ int pvProduction = TypeUtils.max(0, solarEdge.getPvProduction());
+
+ final ApplyPowerHandler.Result apply;
+ if (gridActivePower.isDefined() && essActivePower.isDefined()) {
+ apply = calculate(solarEdge, setActivePower, pvProduction, controlMode, gridActivePower.get(), essActivePower.get());
+ } else {
+ // If any Channel Value is not available: fall back to AUTO mode
+ apply = handleInternalMode(solarEdge);
+ }
+
+ // Set Channels
+ IntegerWriteChannel remoteControlCommandTimeoutChannel = solarEdge.channel(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_TIMEOUT);
+ remoteControlCommandTimeoutChannel.setNextWriteValue(60);
+ IntegerWriteChannel remoteControlCommandChargeLimitChannel = solarEdge.channel(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_CHARGE_LIMIT);
+ remoteControlCommandChargeLimitChannel.setNextWriteValue(apply.chargeLimit);
+ IntegerWriteChannel remoteControlCommandDischargeLimitChannel = solarEdge.channel(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_DISCHARGE_LIMIT);
+ remoteControlCommandDischargeLimitChannel.setNextWriteValue(apply.dischargeLimit);
+ EnumWriteChannel remoteControlCommandModeChannel = solarEdge.channel(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_MODE);
+ remoteControlCommandModeChannel.setNextWriteValue(apply.commandMode);
+ }
+
+ private static record Result(CommandMode commandMode, int chargeLimit, int dischargeLimit) {
+ }
+
+ private static ApplyPowerHandler.Result calculate(SolarEdgeEss solarEdge, int activePowerSetPoint, int pvProduction,
+ ControlMode controlMode, int gridActivePower, int essActivePower)
+ throws OpenemsNamedException {
+ return switch (controlMode) {
+ case INTERNAL //
+ -> handleInternalMode(solarEdge);
+ case SMART //
+ -> handleSmartMode(solarEdge, activePowerSetPoint, pvProduction, gridActivePower, essActivePower);
+ case REMOTE //
+ -> handleRemoteMode(solarEdge, activePowerSetPoint, pvProduction);
+ };
+ }
+
+ private static Result handleInternalMode(SolarEdgeEss solarEdge) {
+ return new Result(CommandMode.AUTO, solarEdge.getBattery1MaxChargeContinuesPower().orElse(0), solarEdge.getBattery1MaxDischargeContinuesPower().orElse(0));
+ }
+
+ private static Result handleSmartMode(SolarEdgeEss solarEdge, int activePowerSetPoint, int pvProduction,
+ int gridActivePower, int essActivePower) throws OpenemsNamedException {
+
+ // Is Surplus-Feed-In active?
+ final var surplusPower = solarEdge.getSurplusPower();
+ var diffSurplus = Integer.MAX_VALUE;
+ if (surplusPower != null && surplusPower > 0 && activePowerSetPoint != 0) {
+ diffSurplus = activePowerSetPoint - surplusPower;
+ }
+
+ // Is Balancing to zero active?
+ var diffBalancing = activePowerSetPoint - (gridActivePower + essActivePower);
+
+ if ((diffBalancing > -1 && diffBalancing < 1 || diffSurplus > -1 && diffSurplus < 1) && activePowerSetPoint != 0) {
+ // avoid rounding errors
+ return handleInternalMode(solarEdge);
+ }
+
+ return handleRemoteMode(solarEdge, activePowerSetPoint, pvProduction);
+ }
+
+ private static Result handleRemoteMode(SolarEdgeEss solarEdge, int activePowerSetPoint, int pvProduction) {
+
+ // TODO PV curtail: (surplus power == setpoint && battery soc == 100% => PV
+ // curtail)
+ if (activePowerSetPoint < 0) {
+ var result = activePowerSetPoint * -1 + pvProduction;
+ if (solarEdge.getSoc().orElse(100) >= 100) {
+ // battery full, limit charge power to zero
+ result = 0;
+ } else if (result > solarEdge.getBattery1MaxChargeContinuesPower().orElse(0)) {
+ // limit to max charge power
+ result = solarEdge.getBattery1MaxChargeContinuesPower().orElse(0);
+ }
+ return new Result(CommandMode.CHARGE_BAT, result, 0);
+ }
+ if (pvProduction >= activePowerSetPoint) {
+ // Set-Point is positive && less than PV-Production -> feed PV partly to grid +
+ // charge battery
+ // On Surplus Feed-In PV == Set-Point => CHARGE_BAT 0
+ var result = pvProduction - activePowerSetPoint;
+ if (result > 0) {
+ var dischargeEfficencyAbsolute = round(activePowerSetPoint * (1 - DISCHARGE_EFFICIENCY_FACTOR)); // Decrease battery charge by DISCHARGE_EFFICIENCY_FACTOR to Power which has to be DC-AC-Converted
+ if (result - dischargeEfficencyAbsolute > 0) {
+ result = result - dischargeEfficencyAbsolute;
+ }
+ }
+ if (solarEdge.getSoc().orElse(100) >= 100) {
+ // battery full, limit charge power to zero -> required for Set-Point 0
+ result = 0;
+ } else if (result > solarEdge.getBattery1MaxChargeContinuesPower().orElse(0)) {
+ // limit to max charge power
+ result = solarEdge.getBattery1MaxChargeContinuesPower().orElse(0);
+ }
+ return new Result(CommandMode.CHARGE_BAT, result, 0);
+ } else {
+ // Set-Point is positive && bigger than PV-Production -> feed all PV to grid +
+ // discharge battery
+ var result = (activePowerSetPoint - pvProduction) + round(activePowerSetPoint * (1 - DISCHARGE_EFFICIENCY_FACTOR)); // Increase battery charge by DISCHARGE_EFFICIENCY_FACTOR to Power which has to be DC-AC-Converted
+ if (solarEdge.getSoc().orElse(0) <= 10) {
+ // battery empty (=SOC equals or less than soc_min of 10), limit charge power to zero -> required for Set-Point 0
+ result = 0;
+ } else if (result > solarEdge.getBattery1MaxDischargeContinuesPower().orElse(0)) {
+ // limit to max discharge power
+ result = solarEdge.getBattery1MaxDischargeContinuesPower().orElse(0);
+ }
+ return new Result(CommandMode.DISCHARGE_BAT, 0, result);
+ }
+ }
+
+ /**
+ * Check if {@link SeControlMode} is set to Remote Control.
+ * If false warning channel REMOTE_CONTROL_NOT_ENABLED is set to true,
+ * otherwise to false.
+ *
+ * @param solarEdge the SolarEdge ESS
+ * @param controlMode the {@link ControlMode} to check control mode
+ */
+ private void checkControlModeRequiresRemoteControl(SolarEdgeEss solarEdge, ControlMode controlMode) {
+ EnumReadChannel seControlModeChannel = solarEdge.channel(SolarEdgeEss.ChannelId.STORAGE_CONTROL_MODE);
+ SeControlMode seControlMode = seControlModeChannel.value().asEnum();
+
+ var enableWarning = switch (seControlMode) {
+ case UNDEFINED -> //
+ // We don't know the Storage Control Mode. Not ready yet (on startup)
+ false;
+
+ case REMOTE_CONTROL ->
+ // Storage Control Mode is set to Remote Control.
+ false;
+
+ case DISABLED, MAX_SELF_CONSUMPTION, TIME_OF_USE, BACKUP_ONLY //
+ -> switch (controlMode) {
+ case INTERNAL ->
+ // INTERNAL mode is ok without Remote Control Mode
+ false;
+ case REMOTE, SMART ->
+ // REMOTE and SMART mode requires Remote Control Mode
+ true;
+ };
+ };
+
+ solarEdge.channel(SolarEdgeEss.ChannelId.REMOTE_CONTROL_NOT_ENABLED).setNextValue(enableWarning);
+ }
+
+ /**
+ * Check if {@link AcChargePolicy} is set to Always allowed.
+ * If false warning channel AC_CHARGE_NOT_ENABLED is set to true,
+ * otherwise to false.
+ *
+ * @param solarEdge the SolarEdge ESS
+ * @param controlMode the {@link ControlMode} to check control mode
+ */
+ private void checkControlModeRequiresAcCharge(SolarEdgeEss solarEdge, ControlMode controlMode) {
+ EnumReadChannel acChargePolicyChannel = solarEdge.channel(SolarEdgeEss.ChannelId.STORAGE_AC_CHARGE_POLICY);
+ AcChargePolicy acChargePolicy = acChargePolicyChannel.value().asEnum();
+
+ var enableWarning = switch (acChargePolicy) {
+ case UNDEFINED -> //
+ // We don't know the AC Charge Policy. Not ready yet (on startup)
+ false;
+
+ case ALWAYS_ALLOWED ->
+ // AC Charge Policy is set to Always allowed.
+ false;
+
+ case DISABLED, FIXED_ENERGY_LIMIT, PERCENT_OF_PRODUCTION //
+ -> switch (controlMode) {
+ case INTERNAL ->
+ // INTERNAL mode is ok with any AC Charge Policy
+ false;
+ case REMOTE, SMART ->
+ // REMOTE and SMART mode requires AC Charge Policy set to Always allowed
+ true;
+ };
+ };
+
+ solarEdge.channel(SolarEdgeEss.ChannelId.AC_CHARGE_NOT_ENABLED).setNextValue(enableWarning);
+ }
+
+ /**
+ * Check current {@link ControlMode} is set to SMART and PID filter is enabled.
+ * If true warning channel SMART_MODE_NOT_WORKING_WITH_PID_FILTER set to true,
+ * otherwise to false.
+ *
+ * @param solarEdge the SolarEdge ESS
+ * @param controlMode the {@link ControlMode} to check SMART mode
+ * @param isPidEnabled if PID filter is enabled
+ */
+ private void checkControlModeWithActivePid(SolarEdgeEss solarEdge, ControlMode controlMode, boolean isPidEnabled) {
+ var enableWarning = false;
+ if (controlMode.equals(ControlMode.SMART) && isPidEnabled) {
+ enableWarning = true;
+ }
+
+ solarEdge.channel(SolarEdgeEss.ChannelId.SMART_MODE_NOT_WORKING_WITH_PID_FILTER).setNextValue(enableWarning);
+ }
+
+ /**
+ * Check if configured {@link ControlMode} is possible - depending on if a
+ * solarEdge Smart Meter is connected or not.
+ *
+ * @param solarEdge the SolarEdge ESS
+ * @param controlMode the {@link ControlMode} to check control mode
+ */
+ private void checkControlModeRequiresSmartMeter(SolarEdgeEss solarEdge, ControlMode controlMode) {
+ EnumReadChannel meterCommunicateStatusChannel = solarEdge.channel(SolarEdgeEss.ChannelId.METER_COMMUNICATE_STATUS);
+ MeterCommunicateStatus meterCommunicateStatus = meterCommunicateStatusChannel.value().asEnum();
+
+ var enableWarning = switch (meterCommunicateStatus) {
+ case UNDEFINED -> //
+ // We don't know if SolarEdge Smart Meter is connected. Not ready yet (on startup)
+ false;
+
+ case OK ->
+ // SolarEdge Smart Meter is connected.
+ false;
+
+ case NO_METER //
+ -> switch (controlMode) {
+ case REMOTE ->
+ // REMOTE mode is ok without SolarEdge Smart Meter
+ false;
+ case INTERNAL, SMART ->
+ // INTERNAL and SMART mode require a SolarEdge Smart Meter
+ true;
+ };
+ };
+
+ solarEdge.channel(SolarEdgeEss.ChannelId.NO_SMART_METER_DETECTED).setNextValue(enableWarning);
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/AverageCalculator.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/AverageCalculator.java
new file mode 100644
index 00000000000..15823f1ca09
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/AverageCalculator.java
@@ -0,0 +1,39 @@
+package io.openems.edge.solaredge.ess;
+
+public class AverageCalculator {
+ private int[] values;
+ private int currentIndex;
+ private int valuesCount;
+
+ public AverageCalculator(int size) {
+ this.values = new int[size];
+ this.currentIndex = 0;
+ this.valuesCount = 0;
+ }
+
+ /**
+ * Adds new value to the rotating array.
+ * @param value Value that has to be added
+ */
+ public void addValue(int value) {
+ this.values[this.currentIndex] = value;
+ this.currentIndex = (this.currentIndex + 1) % this.values.length;
+ if (this.valuesCount < this.values.length) {
+ this.valuesCount++;
+ }
+ }
+
+ /**
+ * Return actual average.
+ * @return average
+ */
+ public int getAverage() {
+ int sum = 0;
+ int count = 0;
+ for (int i = 0; i < this.valuesCount; i++) {
+ sum += this.values[i];
+ count++;
+ }
+ return count > 0 ? sum / count : 0;
+ }
+}
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/Config.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/Config.java
new file mode 100644
index 00000000000..e8aa2faa4a3
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/Config.java
@@ -0,0 +1,43 @@
+package io.openems.edge.solaredge.ess;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+import io.openems.edge.common.type.Phase.SingleOrAllPhase;
+import io.openems.edge.solaredge.enums.ControlMode;
+
+@ObjectClassDefinition(//
+ name = "SolarEdge ESS", //
+ description = "Implements the SolarEdge Energy Storage System.")
+@interface Config {
+
+ @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
+ String id() default "ess0";
+
+ @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 = "PV-Export Limit?", description = "Send the export power site limit to the inverter.")
+ boolean pvExportLimit() default false;
+
+ @AttributeDefinition(name = "Control mode", description = "Sets the Control mode")
+ ControlMode controlMode() default ControlMode.INTERNAL;
+
+ @AttributeDefinition(name = "Modbus-ID", description = "ID of Modbus bridge.")
+ String modbus_id() default "modbus0";
+
+ @AttributeDefinition(name = "Modbus Unit-ID", description = "The Unit-ID of the Modbus device.")
+ int modbusUnitId() default 1;
+
+ @AttributeDefinition(name = "Phase", description = "On which phase is the inverter connected?")
+ SingleOrAllPhase phase() default SingleOrAllPhase.ALL;
+
+ @AttributeDefinition(name = "Modbus target filter", description = "This is auto-generated by 'Modbus-ID'.")
+ String Modbus_target() default "(enabled=true)";
+
+ String webconsole_configurationFactory_nameHint() default "io.openems.edge.solaredge.ess [{id}]";
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/IgnoreMinPowerConverter.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/IgnoreMinPowerConverter.java
new file mode 100644
index 00000000000..9dea81862cf
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/IgnoreMinPowerConverter.java
@@ -0,0 +1,47 @@
+package io.openems.edge.solaredge.ess;
+
+import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
+
+/**
+ * Even if there is no real power from PV and no Charge/Discharge Power, the Inverter Power Channel
+ * could remain on minimum power values. These values are ignored.
+ *
+ *
+ * The class optionally creates a {@link ElementToChannelConverterChain}, for
+ * use-cases when additionally to the above 'IgnoeMinPower' logic a scale-factor is
+ * required.
+ */
+public class IgnoreMinPowerConverter extends ElementToChannelConverter {
+
+ /**
+ * Generates an ElementToChannelConverter for the use case covered by
+ * {@link IgnoreMinPowerConverter}.
+ *
+ * @param parent the parent component
+ * @param converter an additional {@link ElementToChannelConverter}
+ * @return the {@link ElementToChannelConverter}
+ */
+ public static ElementToChannelConverter from(SolarEdgeEssImpl parent,
+ ElementToChannelConverter converter) {
+ if (converter == DIRECT_1_TO_1) {
+ return new IgnoreMinPowerConverter(parent);
+ }
+ return ElementToChannelConverter.chain(new IgnoreMinPowerConverter(parent), converter);
+ }
+
+ private IgnoreMinPowerConverter(SolarEdgeEssImpl parent) {
+ super(value -> {
+ // Is value null?
+ if (value == null) {
+ return null;
+ }
+ // If there is no PV Production and no Charge/Discharge Power -> ignore minimum values
+ if (value instanceof Float f && Math.abs(f) < 50 && parent.getPvProduction() != null && parent.getPvProduction() == 0
+ && parent.getDcDischargePower().orElse(-1) == 0) {
+ return 0;
+ }
+ return value;
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/SetPvExportLimitHandler.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/SetPvExportLimitHandler.java
new file mode 100644
index 00000000000..8d5fb027701
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/SetPvExportLimitHandler.java
@@ -0,0 +1,92 @@
+package io.openems.edge.solaredge.ess;
+
+
+import java.util.Optional;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.exceptions.OpenemsException;
+import io.openems.common.function.ThrowingConsumer;
+import io.openems.edge.common.channel.FloatWriteChannel;
+import io.openems.edge.common.channel.IntegerReadChannel;
+
+public class SetPvExportLimitHandler implements ThrowingConsumer, OpenemsNamedException> {
+
+ private static final float COMPARE_THRESHOLD = 0.0001F;
+
+ private final SolarEdgeEss parent;
+
+ public SetPvExportLimitHandler(SolarEdgeEss parent) {
+ this.parent = parent;
+ }
+
+ /**
+ * Handles a PV-Inverter Export power limitation request.
+ *
+ * @param activeExportPowerLimitOpt an optional export power site limit; if present,
+ * it updates the inverter's export site limit
+ * (used for power manager inverters with a connected smart meter)
+ * @throws OpenemsNamedException on error
+ */
+ @Override
+ public void accept(Optional activeExportPowerLimitOpt) throws OpenemsNamedException {
+ FloatWriteChannel wMaxLimPwrChannel;
+ IntegerReadChannel exportControlModeChannel;
+ IntegerReadChannel advancedPwrControlEnChannel;
+
+ // Get Export Power Limitation Channel
+ wMaxLimPwrChannel = this.parent.channel(SolarEdgeEss.ChannelId.EXPORT_CONTROL_SITE_LIMIT);
+
+ // Get Export Control Mode Channel
+ exportControlModeChannel = this.parent.channel(SolarEdgeEss.ChannelId.EXPORT_CONTROL_MODE);
+
+ // Get Get AdvancedPwrControlEn Channel
+ advancedPwrControlEnChannel = this.parent.channel(SolarEdgeEss.ChannelId.ADVANCED_PWR_CONTROL_EN);
+
+ if (advancedPwrControlEnChannel.value().get() == null || exportControlModeChannel.value().get() == null) {
+ // We don't know the channel values. Not ready yet (on startup)
+ return;
+ }
+
+ int advancedPwrControlEn = advancedPwrControlEnChannel.value().get();
+ int exportControlMode = exportControlModeChannel.value().get();
+ int exportControlModeDirectExportLimitation = 1 & exportControlMode;
+ int exportControlModeInDirectExportLimitation = 2 & exportControlMode;
+ int productionLimitation = 4 & exportControlMode;
+
+ if (advancedPwrControlEn == 0 || (exportControlModeDirectExportLimitation == 0 && exportControlModeInDirectExportLimitation == 0 && productionLimitation == 0)) {
+ // Inverter configuration not as required ,...
+ if (activeExportPowerLimitOpt.isPresent()) {
+ // and power should be limited -> throw error
+ if (advancedPwrControlEn == 0) {
+ throw new OpenemsException("PV Export Limit Control requested, but inverter configuration is wrong (AdvancedPwrControl not enabled).");
+ } else {
+ throw new OpenemsException("PV Export Limit Control requested, but inverter configuration is wrong (Export Limitation not enabled).");
+ }
+ }
+ // and no power limitation is required -> ignore error and exit
+ return;
+ }
+
+ if (activeExportPowerLimitOpt.isPresent()) {
+ /*
+ * A ActiveExportPowerLimit is set
+ */
+ float activeExportPowerLimit = activeExportPowerLimitOpt.get();
+
+ // keep export power limit in range [>=0].
+ if (activeExportPowerLimit < 0) {
+ activeExportPowerLimit = 0;
+ }
+
+ if (
+ // No value set
+ !wMaxLimPwrChannel.value().isDefined()
+ // or value changed
+ || Math.abs(activeExportPowerLimit - wMaxLimPwrChannel.value().get()) > COMPARE_THRESHOLD) {
+
+ // Set Export Power Limitation
+ wMaxLimPwrChannel.setNextWriteValue(activeExportPowerLimit);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/SolarEdgeEss.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/SolarEdgeEss.java
new file mode 100644
index 00000000000..b02219b67dd
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/SolarEdgeEss.java
@@ -0,0 +1,915 @@
+package io.openems.edge.solaredge.ess;
+
+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.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.types.OpenemsType;
+
+import io.openems.edge.common.channel.Channel;
+import io.openems.edge.common.channel.Doc;
+import io.openems.edge.common.channel.FloatReadChannel;
+import io.openems.edge.common.channel.IntegerDoc;
+import io.openems.edge.common.channel.IntegerReadChannel;
+import io.openems.edge.common.channel.IntegerWriteChannel;
+import io.openems.edge.common.channel.LongReadChannel;
+import io.openems.edge.common.channel.value.Value;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable;
+import io.openems.edge.ess.api.SymmetricEss;
+import io.openems.edge.solaredge.charger.SolarEdgeCharger;
+import io.openems.edge.solaredge.enums.AcChargePolicy;
+import io.openems.edge.solaredge.enums.BatteryStatus;
+import io.openems.edge.solaredge.enums.CommandMode;
+import io.openems.edge.solaredge.enums.MeterCommunicateStatus;
+import io.openems.edge.solaredge.enums.SeControlMode;
+
+public interface SolarEdgeEss extends OpenemsComponent, SymmetricEss {
+
+ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId {
+ WRONG_PHASE_CONFIGURED(Doc.of(Level.WARNING) //
+ .text("Configured Phase does not match the Model")), //
+ SMART_MODE_NOT_WORKING_WITH_PID_FILTER(Doc.of(Level.WARNING) //
+ .text("SMART mode does not work correctly with active PID filter")),
+ NO_SMART_METER_DETECTED(Doc.of(Level.WARNING) //
+ .text("No SolarEdge Smart Meter detected. Only REMOTE mode can work correctly")),
+ REMOTE_CONTROL_NOT_ENABLED(Doc.of(Level.WARNING) //
+ .text("Storage Control Mode is not set to Remote Control. Please configure inverter using SetApp/LCD")),
+ AC_CHARGE_NOT_ENABLED(Doc.of(Level.WARNING) //
+ .text("Storage AC Charge Policy is not set to Always allowed. Please configure inverter using SetApp/LCD")),
+ PV_EXPORT_LIMIT_FAILED(Doc.of(Level.FAULT) //
+ .text("PV-Export Limit failed")), //
+ DISABLED_PV_EXPORT_LIMIT_FAILED(Doc.of(Level.WARNING) //
+ .text("PV-Export Limit is disabled: PV-Export Limit failed")), //
+
+ SERIAL_NUMBER(Doc.of(OpenemsType.STRING) //
+ .persistencePriority(PersistencePriority.HIGH)),
+
+ /**
+ * Read/Set Active Export Power Limit.
+ *
+ *
+ * - Interface: FeedToGridLimitEss
+ *
- Type: Integer
+ *
- Unit: W
+ *
+ */
+ ACTIVE_EXPORT_POWER_LIMIT(new IntegerDoc() //
+ .unit(Unit.WATT) //
+ .accessMode(AccessMode.READ_WRITE) //
+ .persistencePriority(PersistencePriority.MEDIUM) //
+ .onInit(channel -> { //
+ // on each Write to the channel -> set the value
+ ((IntegerWriteChannel) channel).onSetNextWrite(value -> {
+ channel.setNextValue(value);
+ });
+ })),
+
+ /**
+ * Storage Control Mode is used to set the StorEdge system operating mode.
+ *
+ * - 0 – Disabled
+ *
- 1 – Maximize Self Consumption – requires a SolarEdge Electricity meter
+ * on the grid or load connection point
+ *
- 2 – Time of Use (Profile programming) – requires a SolarEdge Electricity meter
+ * on the grid or load connection point
+ *
- 3 – Backup Only (applicable only for systems support backup functionality)
+ *
- 4 – Remote Control – the battery charge/discharge state is controlled by an
+ * external controller
+ *
+ */
+ STORAGE_CONTROL_MODE(Doc.of(SeControlMode.values()).accessMode(AccessMode.READ_ONLY)),
+
+ /**
+ * Defines the AC charge policy for the storage system.
+ *
+ * - 0 - Disable: No AC charging allowed.
+ *
- 1 - Always allowed: Essential for AC coupling operation. Enables
+ * unlimited charging from AC. In 'Maximize Self-Consumption' mode, charging
+ * occurs only with excess power; grid charging is prohibited.
+ *
- 2 - Fixed Energy Limit: Allows AC charging up to a fixed yearly limit
+ * (Jan 1 to Dec 31), crucial for ITC regulation compliance in the US.
+ *
- 3 - Percent of Production: Permits AC charging up to a percentage of the
+ * system's year-to-date production, also for ITC regulation in the US.
+ *
+ */
+ STORAGE_AC_CHARGE_POLICY(Doc.of(AcChargePolicy.values()).accessMode(AccessMode.READ_ONLY)),
+
+ /**
+ * Storage AC Charge Limit is used to set the AC charge limit according to the
+ * policy set in the previous register. Either fixed in kWh or percentage is set
+ * (e.g. 100KWh or 70%). Relevant only for Storage AC Charge Policy = 2 or 3
+ */
+ STORAGE_AC_CHARGE_LIMIT(Doc.of(OpenemsType.INTEGER) // Percent or kWh
+ .unit(Unit.PERCENT) //
+ .persistencePriority(PersistencePriority.HIGH).accessMode(AccessMode.READ_ONLY)), // defined in external
+
+ /**
+ * Storage Backup Reserved Setting sets the percentage of reserved battery SOE
+ * to be used for backup purposes. Relevant only for inverters with backup
+ * functionality.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: Percent
+ *
-
+ *
+ */
+ STORAGE_BACKUP_RESERVED_SETTING(Doc.of(OpenemsType.INTEGER) // Percent. Relevant only for inverters with backup functionality.
+ .unit(Unit.PERCENT) //
+ .persistencePriority(PersistencePriority.HIGH).accessMode(AccessMode.READ_ONLY)),
+
+ /**
+ * Charge/Discharge default Mode / Remote Control Command Mode Storage
+ * Charge/Discharge default Mode sets the default mode of operation when Remote
+ * Control Command Timeout has expired. The supported Charge/Discharge Modes are
+ * as follows: 0 – Off 1 – Charge excess PV power only. Only PV excess power not
+ * going to AC is used for charging the battery. Inverter
+ * NominalActivePowerLimit (or the inverter rated power whichever is lower) sets
+ * how much power the inverter is producing to the AC. In this mode, the battery
+ * cannot be discharged. If the PV power is lower than NominalActivePowerLimit
+ * the AC production will be equal to the PV power. 2 – Charge from PV first,
+ * before producing power to the AC. The Battery charge has higher priority than
+ * AC production. First charge the battery then produce AC. If
+ * StorageRemoteCtrl_ChargeLimit is lower than PV excess power goes to AC
+ * according to NominalActivePowerLimit. If NominalActivePowerLimit is reached
+ * and battery StorageRemoteCtrl_ChargeLimit is reached, PV power is curtailed.
+ * 3 – Charge from PV+AC according to the max battery power. Charge from both PV
+ * and AC with priority on PV power. If PV production is lower than
+ * StorageRemoteCtrl_ChargeLimit, the battery will be charged from AC up to
+ * NominalActivePow-erLimit. In this case AC power =
+ * StorageRemoteCtrl_ChargeLimit- PVpower. If PV power is larger than
+ * StorageRemoteCtrl_ChargeLimit the excess PV power will be directed to the AC
+ * up to the Nominal-ActivePowerLimit beyond which the PV is curtailed. 4 –
+ * Maximize export – discharge battery to meet max inverter AC limit. AC power
+ * is maintained to NominalActivePowerLimit, using PV power and/or battery
+ * power. If the PV power is not sufficient, battery power is used to complement
+ * AC power up to StorageRemoteCtrl_DishargeLimit. In this mode, charging excess
+ * power will occur if there is more PV than the AC limit. 5 – Discharge to meet
+ * loads consumption. Discharging to the grid is not allowed. 7 – Maximize
+ * self-consumption
+ */
+ STORAGE_CHARGE_DISCHARGE_DEFAULT_MODE(Doc.of(CommandMode.values()).accessMode(AccessMode.READ_ONLY)),
+
+ /**
+ * Remote Control Command Timeout sets the operating timeframe for the
+ * charge/discharge command sets in Remote Control.
+ */
+ DEBUG_REMOTE_CONTROL_COMMAND_TIMEOUT(Doc.of(OpenemsType.INTEGER).unit(Unit.SECONDS)), //
+ REMOTE_CONTROL_COMMAND_TIMEOUT(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_WRITE).unit(Unit.SECONDS) //
+ .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_REMOTE_CONTROL_COMMAND_TIMEOUT)), //), //
+
+ /**
+ * Charge/Discharge default Mode / Remote Control Command Mode Storage
+ * Charge/Discharge default Mode sets the default mode of operation when Remote
+ * Control Command Timeout has expired. The supported Charge/Discharge Modes are
+ * as follows: 0 – Off 1 – Charge excess PV power only. Only PV excess power not
+ * going to AC is used for charging the battery. Inverter
+ * NominalActivePowerLimit (or the inverter rated power whichever is lower) sets
+ * how much power the inverter is producing to the AC. In this mode, the battery
+ * cannot be discharged. If the PV power is lower than NominalActivePowerLimit
+ * the AC production will be equal to the PV power. 2 – Charge from PV first,
+ * before producing power to the AC. The Battery charge has higher priority than
+ * AC production. First charge the battery then produce AC. If
+ * StorageRemoteCtrl_ChargeLimit is lower than PV excess power goes to AC
+ * according to NominalActivePowerLimit. If NominalActivePowerLimit is reached
+ * and battery StorageRemoteCtrl_ChargeLimit is reached, PV power is curtailed.
+ * 3 – Charge from PV+AC according to the max battery power. Charge from both PV
+ * and AC with priority on PV power. If PV production is lower than
+ * StorageRemoteCtrl_ChargeLimit, the battery will be charged from AC up to
+ * NominalActivePow-erLimit. In this case AC power =
+ * StorageRemoteCtrl_ChargeLimit- PVpower. If PV power is larger than
+ * StorageRemoteCtrl_ChargeLimit the excess PV power will be directed to the AC
+ * up to the Nominal-ActivePowerLimit beyond which the PV is curtailed. 4 –
+ * Maximize export – discharge battery to meet max inverter AC limit. AC power
+ * is maintained to NominalActivePowerLimit, using PV power and/or battery
+ * power. If the PV power is not sufficient, battery power is used to complement
+ * AC power up to StorageRemoteCtrl_DishargeLimit. In this mode, charging excess
+ * power will occur if there is more PV than the AC limit. 5 – Discharge to meet
+ * loads consumption. Discharging to the grid is not allowed. 7 – Maximize
+ * self-consumption
+ */
+ DEBUG_REMOTE_CONTROL_COMMAND_MODE(Doc.of(CommandMode.values())),
+ REMOTE_CONTROL_COMMAND_MODE(Doc.of(CommandMode.values())
+ .accessMode(AccessMode.READ_WRITE)
+ .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_REMOTE_CONTROL_COMMAND_MODE)), //
+
+ /**
+ * Maximum Charge Power Channel. Always positive. Reads and Writes the charge power
+ * Control mode and charge policy have to be set
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Integer
+ *
- Unit: W
+ *
+ */
+ DEBUG_REMOTE_CONTROL_COMMAND_CHARGE_LIMIT(Doc.of(OpenemsType.INTEGER)),
+ REMOTE_CONTROL_COMMAND_CHARGE_LIMIT(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.WATT) //
+ .accessMode(AccessMode.READ_WRITE)
+ .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_REMOTE_CONTROL_COMMAND_CHARGE_LIMIT)),
+
+ /**
+ * Maximum Discharge Power Channel. Always positive. Reads and writes the discharge power.
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Integer
+ *
- Unit: W
+ *
+ */
+ DEBUG_REMOTE_CONTROL_COMMAND_DISCHARGE_LIMIT(Doc.of(OpenemsType.INTEGER)),
+ REMOTE_CONTROL_COMMAND_DISCHARGE_LIMIT(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.WATT) //
+ .accessMode(AccessMode.READ_WRITE)
+ .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_REMOTE_CONTROL_COMMAND_DISCHARGE_LIMIT)),
+
+ /**
+ * Battery 1 Max Charge Continues Power. Varies with SoC.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: W
+ *
-
+ *
+ */
+ BATTERY1_MAX_CHARGE_CONTINUES_POWER(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.WATT) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Max Discharge Continues Power. Varies with SoC.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: W
+ *
-
+ *
+ */
+ BATTERY1_MAX_DISCHARGE_CONTINUES_POWER(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.WATT) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Max Charge Peak Power. Varies with SoC. ?????
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: W
+ *
-
+ *
+ */
+ BATTERY1_MAX_CHARGE_PEAK_POWER(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.WATT) //
+ .persistencePriority(PersistencePriority.LOW)), // defined in external file
+
+ /**
+ * Battery 1 Max Discharge Peak Power. Varies with SoC. ?????
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: W
+ *
-
+ *
+ */
+ BATTERY1_MAX_DISCHARGE_PEAK_POWER(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.WATT) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Average Temperature.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: °C
+ *
-
+ *
+ */
+ BATTERY1_AVG_TEMPERATURE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.DEGREE_CELSIUS) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Max Temperature.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: W
+ *
-
+ *
+ */
+ BATTERY1_MAX_TEMPERATURE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.DEGREE_CELSIUS) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Actual Voltage.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: V
+ *
-
+ *
+ */
+ BATTERY1_ACTUAL_VOLTAGE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.VOLT) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Actual Current to or from the battery.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: mA
+ *
-
+ *
+ */
+ BATTERY1_ACTUAL_CURRENT(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Actual Charge/Discharge Power.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: W
+ *
- Range: positive values for Charge; negative for Discharge
+ *
- This is the instantaneous power to or from the battery
+ *
+ */
+ BATTERY1_ACTUAL_POWER(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.WATT) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Lifetime Export Energy Counter. "Lifetime" resets every night. Channel not
+ * really useful!
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: Wh
+ *
-
+ *
+ */
+ BATTERY1_LIFETIME_EXPORT_ENERGY(Doc.of(OpenemsType.LONG) //
+ .unit(Unit.WATT_HOURS) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Lifetime Import Energy Counter. "Lifetime" resets every night. No useful
+ * information!
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: Wh
+ *
-
+ *
+ */
+ BATTERY1_LIFETIME_IMPORT_ENERGY(Doc.of(OpenemsType.LONG) //
+ .unit(Unit.WATT_HOURS) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 Max. Capacity.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: Wh
+ *
-
+ *
+ */
+ BATTERY1_MAX_CAPACITY(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.WATT_HOURS) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Battery 1 State Of Health.
+ *
+ *
+ * - Interface: Ess
+ *
- Type: Integer
+ *
- Unit: Percent
+ *
-
+ *
+ */
+ SOH(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.PERCENT) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Batter 1 Status. SE_BATT_STATUS_OFF(0, "Off"), //
+ * SE_BATT_STATUS_STBY(1, "Standby"), // SE_BATT_STATUS_INIT(2, "Init"), //
+ * SE_BATT_STATUS_CHARGE(3, "Charge"), // SE_BATT_STATUS_DISCHARGE(4,
+ * "Discharge"), // SE_BATT_STATUS_FAULT(5, "Fault"), // // 6 doesn´t exist
+ * SE_BATT_STATUS_IDLE(7, "Idle"); //
+ *
+ * - Interface: Ess
+ *
- Type: enum
+ *
- Unit:
+ *
-
+ *
+ */
+ BATTERY1_STATUS(Doc.of(BatteryStatus.values()).accessMode(AccessMode.READ_ONLY)),
+
+ /**
+ * Inverter Actual DC Power.
+ *
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Integer
+ *
- Unit: W
+ *
-
+ *
+ */
+ INVERTER_ACTIVE_DC_POWER(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.WATT) //
+ .persistencePriority(PersistencePriority.HIGH)),
+
+ /**
+ * Inverter Max Apparent Power.
+ *
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Float
+ *
- Unit: Percent
+ *
-
+ *
+ */
+ INVERTER_MAX_APPARENT_POWER(Doc.of(OpenemsType.FLOAT) //
+ .unit(Unit.WATT) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Power Control Fixed Power Limit.
+ *
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Float
+ *
- Unit: Percent
+ *
-
+ *
+ */
+ INVERTER_POWER_LIMIT(Doc.of(OpenemsType.FLOAT) //
+ .unit(Unit.PERCENT) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /**
+ * Advanced Power Control Enabled.
+ *
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Integer
+ *
-
+ *
+ */
+ ADVANCED_PWR_CONTROL_EN(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY)),
+
+ /**
+ * Export Control Mode.
+ *
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Integer
+ *
-
+ *
+ */
+ EXPORT_CONTROL_MODE(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY)),
+
+ /**
+ * Export Control Limit Mode.
+ *
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Integer
+ *
-
+ *
+ */
+ EXPORT_CONTROL_LIMIT_MODE(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY)),
+
+ /**
+ * Export Control Site Limit.
+ *
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Float
+ *
- Unit: Watt
+ *
-
+ *
+ */
+ EXPORT_CONTROL_SITE_LIMIT(Doc.of(OpenemsType.FLOAT) //
+ .unit(Unit.WATT) //
+ .persistencePriority(PersistencePriority.HIGH)
+ .accessMode(AccessMode.READ_WRITE)),
+
+ /**
+ * Active Production Energy.
+ *
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Long
+ *
- Unit: CUMULATED_WATT_HOURS
+ *
- Range: only positive values
+ *
+ */
+ ACTIVE_PRODUCTION_ENERGY(Doc.of(OpenemsType.LONG) //
+ .unit(Unit.CUMULATED_WATT_HOURS) //
+ .persistencePriority(PersistencePriority.HIGH)),
+
+ /**
+ * DC-Voltage produced by the ESS. Either for grid or consumption.
+ *
+ *
+ * - Interface: SolarEdgeEss
+ *
- Type: Integer
+ *
- Unit: mV
+ *
-
+ *
+ */
+ VOLTAGE_DC(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.MILLIVOLT) //
+ .persistencePriority(PersistencePriority.LOW)),
+
+ /*
+ *
+ * METER_COMMUNICATE_STATUS
+ *
+ */
+ METER_COMMUNICATE_STATUS(Doc.of(MeterCommunicateStatus.values())), //
+
+ ;
+
+ private final Doc doc;
+
+ private ChannelId(Doc doc) {
+ this.doc = doc;
+ }
+
+ @Override
+ public Doc doc() {
+ return this.doc;
+ }
+
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#ACTIVE_EXPORT_POWER_LIMIT}.
+ *
+ * @return the Channel
+ */
+ public default IntegerWriteChannel getActiveExportPowerLimitChannel() {
+ return this.channel(ChannelId.ACTIVE_EXPORT_POWER_LIMIT);
+ }
+
+ /**
+ * Gets the Active Export Power Limit in [W]. See {@link ChannelId#ACTIVE_EXPORT_POWER_LIMIT}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getActiveExportPowerLimit() {
+ return this.getActiveExportPowerLimitChannel().value();
+ }
+
+ /**
+ * Sets the Active Export Power Limit in [W]. See {@link ChannelId#ACTIVE_EXPORT_POWER_LIMIT}.
+ *
+ * @param value the Integer value
+ * @throws OpenemsNamedException on error
+ */
+ public default void setActiveExportPowerLimit(Integer value) throws OpenemsNamedException {
+ this.getActiveExportPowerLimitChannel().setNextWriteValue(value);
+ }
+
+ /**
+ * Sets the Active Export Power Limit in [W]. See {@link ChannelId#ACTIVE_EXPORT_POWER_LIMIT}.
+ *
+ * @param value the int value
+ * @throws OpenemsNamedException on error
+ */
+ public default void setActiveExportPowerLimit(int value) throws OpenemsNamedException {
+ this.getActiveExportPowerLimitChannel().setNextWriteValue(value);
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#STORAGE_CONTROL_MODE}.
+ *
+ * @return the Channel
+ */
+ public default Channel getStorageControlModeChannel() {
+ return this.channel(ChannelId.STORAGE_CONTROL_MODE);
+ }
+
+ /**
+ * See {@link ChannelId#STORAGE_CONTROL_MODE}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default SeControlMode getStorageControlMode() {
+ return this.getStorageControlModeChannel().value().asEnum();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#STORAGE_AC_CHARGE_POLICY}.
+ *
+ * @return the Channel
+ */
+ public default Channel getStorageAcChargePolicyChannel() {
+ return this.channel(ChannelId.STORAGE_AC_CHARGE_POLICY);
+ }
+
+ /**
+ * AC charge policy {@link ChannelId#STORAGE_AC_CHARGE_POLICY}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default AcChargePolicy getStorageAcChargePolicy() {
+ return this.getStorageAcChargePolicyChannel().value().asEnum();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#STORAGE_AC_CHARGE_LIMIT}.
+ *
+ * @return the Channel
+ */
+ public default Channel getStorageAcChargeLimitChannel() {
+ return this.channel(ChannelId.STORAGE_AC_CHARGE_LIMIT);
+ }
+
+ /**
+ * AC charge policy {@link ChannelId#STORAGE_AC_CHARGE_LIMIT}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default SeControlMode getStorageAcChargeLimit() {
+ return this.getStorageAcChargeLimitChannel().value().asEnum();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#STORAGE_BACKUP_RESERVED_SETTING}.
+ *
+ * @return the Channel
+ */
+ public default Channel getStorageBackupReservedSettingChannel() {
+ return this.channel(ChannelId.STORAGE_BACKUP_RESERVED_SETTING);
+ }
+
+ /**
+ * See {@link ChannelId#STORAGE_BACKUP_RESERVED_SETTING}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default SeControlMode getStorageBackupReservedSetting() {
+ return this.getStorageBackupReservedSettingChannel().value().asEnum();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#CHARGE_DISCHARGE_DEFAULT_MODE}.
+ *
+ * @return the Channel
+ */
+ public default Channel getStorageChargeDischargeDefaultModeChannel() {
+ return this.channel(ChannelId.STORAGE_CHARGE_DISCHARGE_DEFAULT_MODE);
+ }
+
+ /**
+ * See {@link ChannelId#CHARGE_DISCHARGE_DEFAULT_MODE}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default SeControlMode getStorageChargeDischargeDefaultMode() {
+ return this.getStorageChargeDischargeDefaultModeChannel().value().asEnum();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#BATTERY1_MAX_CHARGE_CONTINUES_POWER}.
+ *
+ * @return the Channel
+ */
+ public default IntegerReadChannel getBattery1MaxChargeContinuesPowerChannel() {
+ return this.channel(ChannelId.BATTERY1_MAX_CHARGE_CONTINUES_POWER);
+ }
+
+ /**
+ * See {@link ChannelId#BATTERY1_MAX_CHARGE_CONTINUES_POWER}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getBattery1MaxChargeContinuesPower() {
+ return this.getBattery1MaxChargeContinuesPowerChannel().value();
+ }
+
+
+ /**
+ * Gets the Channel for {@link ChannelId#BATTERY1_MAX_DISCHARGE_CONTINUES_POWER}.
+ *
+ * @return the Channel
+ */
+ public default IntegerReadChannel getBattery1MaxDischargeContinuesPowerChannel() {
+ return this.channel(ChannelId.BATTERY1_MAX_DISCHARGE_CONTINUES_POWER);
+ }
+
+ /**
+ * See {@link ChannelId#BATTERY1_MAX_DISCHARGE_CONTINUES_POWER}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getBattery1MaxDischargeContinuesPower() {
+ return this.getBattery1MaxDischargeContinuesPowerChannel().value();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#BATTERY1_MAX_CHARGE_PEAK_POWER}.
+ *
+ * @return the Channel
+ */
+ public default IntegerReadChannel getBattery1MaxChargePeakPowerChannel() {
+ return this.channel(ChannelId.BATTERY1_MAX_CHARGE_PEAK_POWER);
+ }
+
+ /**
+ * See {@link ChannelId#BATTERY1_MAX_CHARGE_PEAK_POWER}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getBattery1MaxChargePeakPower() {
+ return this.getBattery1MaxChargePeakPowerChannel().value();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#BATTERY1_MAX_DISCHARGE_PEAK_POWER}.
+ *
+ * @return the Channel
+ */
+ public default IntegerReadChannel getBattery1MaxDischargePeakPowerChannel() {
+ return this.channel(ChannelId.BATTERY1_MAX_DISCHARGE_PEAK_POWER);
+ }
+
+ /**
+ * See {@link ChannelId#BATTERY1_MAX_DISCHARGE_PEAK_POWER}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getBattery1MaxDischargePeakPower() {
+ return this.getBattery1MaxDischargePeakPowerChannel().value();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#BATTERY1_ACTUAL_POWER}.
+ *
+ * @return the Channel
+ */
+ public default IntegerReadChannel getBattery1ActualPowerChannel() {
+ return this.channel(ChannelId.BATTERY1_ACTUAL_POWER);
+ }
+
+ /**
+ * AC-Power produced by ESS. See {@link ChannelId#BATTERY1_ACTUAL_POWER}
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getBattery1ActualPower() {
+ return this.getBattery1ActualPowerChannel().value();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#BATTERY1_LIFETIME_EXPORT_ENERGY}.
+ *
+ * @return the Channel
+ */
+ public default LongReadChannel getBattery1LifetimeExportEnergyChannel() {
+ return this.channel(ChannelId.BATTERY1_LIFETIME_EXPORT_ENERGY);
+ }
+
+ /**
+ * Gets the Actual Energy in [Wh_Σ]. See
+ * {@link ChannelId#BATTERY1_LIFETIME_EXPORT_ENERGY}
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getBattery1LifetimeExportEnergy() {
+ return this.getBattery1LifetimeExportEnergyChannel().value();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#BATTERY1_LIFETIME_IMPORT_ENERGY}.
+ *
+ * @return the Channel
+ */
+ public default LongReadChannel getBattery1LifetimeImportEnergyChannel() {
+ return this.channel(ChannelId.BATTERY1_LIFETIME_IMPORT_ENERGY);
+ }
+
+ /**
+ * See {@link ChannelId#BATTERY1_LIFETIME_IMPORT_ENERGY}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getBattery1LifetimeImportEnergy() {
+ return this.getBattery1LifetimeImportEnergyChannel().value();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#INVERTER_POWER_LIMIT}.
+ *
+ * @return the Channel
+ */
+ public default FloatReadChannel getInverterPowerLimitChannel() {
+ return this.channel(ChannelId.INVERTER_POWER_LIMIT);
+ }
+
+ /**
+ * See {@link ChannelId#INVERTER_POWER_LIMIT}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getInverterPowerLimit() {
+ return this.getInverterPowerLimitChannel().value();
+ }
+
+ /**
+ * Gets the Channel for {@link ChannelId#ACTIVE_PRODUCTION_ENERGY}.
+ *
+ * @return the Channel
+ */
+ public default LongReadChannel getActiveProductionEnergyChannel() {
+ return this.channel(ChannelId.ACTIVE_PRODUCTION_ENERGY);
+ }
+
+ /**
+ * See {@link ChannelId#ACTIVE_PRODUCTION_ENERGY}.
+ *
+ * @return the Channel {@link Value}
+ */
+ public default Value getActiveProductionEnergy() {
+ return this.getActiveProductionEnergyChannel().value();
+ }
+
+ /**
+ * Adds DC-charger to ESS hybrid system. Represents PV production
+ *
+ * @param charger link to DC charger(s)
+ */
+ public void addCharger(SolarEdgeCharger charger);
+
+ /**
+ * Removes link to pv DC charger.
+ *
+ * @param charger charger
+ */
+ public void removeCharger(SolarEdgeCharger charger);
+
+ /**
+ * returns ModbusBrdigeId from config.
+ *
+ * @return ModbusBrdigeId from config
+ */
+ public String getModbusBridgeId();
+
+ /**
+ * returns UnitId for ESS from config.
+ *
+ * @return UnitId for ESS from config
+ */
+ public Integer getUnitId();
+
+
+ /**
+ * Gets the PV production from chargers ACTUAL_POWER. Returns null if the PV
+ * production is not available.
+ *
+ * @return production power
+ */
+ public Integer getPvProduction();
+
+ /**
+ * Gets Surplus Power.
+ *
+ * @return {@link Integer}
+ */
+ public Integer getSurplusPower();
+
+ /**
+ * Used for Modbus/TCP Api Controller. Provides a Modbus table for the Channels
+ * of this Component.
+ *
+ * @param accessMode filters the Modbus-Records that should be shown
+ * @return the {@link ModbusSlaveNatureTable}
+ */
+ public default ModbusSlaveNatureTable getModbusSlaveNatureTable(AccessMode accessMode) {
+ return ModbusSlaveNatureTable.of(SolarEdgeEss.class, accessMode, 100) //
+ .build();
+ }
+
+}
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/SolarEdgeEssImpl.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/SolarEdgeEssImpl.java
new file mode 100644
index 00000000000..3d8dab93154
--- /dev/null
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/ess/SolarEdgeEssImpl.java
@@ -0,0 +1,776 @@
+package io.openems.edge.solaredge.ess;
+
+import static io.openems.edge.common.cycle.Cycle.DEFAULT_CYCLE_TIME;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.DIRECT_1_TO_1;
+import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_3;
+import static java.time.temporal.ChronoUnit.MILLIS;
+
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+import org.osgi.service.event.propertytypes.EventTopics;
+import io.openems.edge.common.event.EdgeEventConstants;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import io.openems.common.channel.AccessMode;
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.exceptions.OpenemsException;
+import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel;
+import io.openems.edge.bridge.modbus.sunspec.SunSpecModel;
+import io.openems.edge.bridge.modbus.sunspec.ess.AbstractSunSpecEss;
+import io.openems.edge.bridge.modbus.api.BridgeModbus;
+import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
+import io.openems.edge.bridge.modbus.api.ModbusComponent;
+import io.openems.edge.bridge.modbus.api.ModbusProtocol;
+import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
+import io.openems.edge.bridge.modbus.api.element.FloatDoublewordElement;
+import io.openems.edge.bridge.modbus.api.element.SignedDoublewordElement;
+import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement;
+import io.openems.edge.bridge.modbus.api.element.UnsignedQuadruplewordElement;
+import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
+import io.openems.edge.bridge.modbus.api.element.WordOrder;
+import io.openems.edge.bridge.modbus.api.task.FC16WriteRegistersTask;
+import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
+import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S1;
+import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S101;
+import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S102;
+import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S103;
+import io.openems.edge.bridge.modbus.sunspec.FilteredSunSpecModel;
+import io.openems.edge.common.channel.FloatReadChannel;
+import io.openems.edge.common.channel.IntegerReadChannel;
+import io.openems.edge.common.channel.IntegerWriteChannel;
+import io.openems.edge.common.channel.value.Value;
+import io.openems.edge.common.component.ComponentManager;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.common.cycle.Cycle;
+import io.openems.edge.common.modbusslave.ModbusSlave;
+import io.openems.edge.common.modbusslave.ModbusSlaveTable;
+import io.openems.edge.common.sum.GridMode;
+import io.openems.edge.common.sum.Sum;
+import io.openems.edge.common.taskmanager.Priority;
+import io.openems.edge.common.type.Phase.SingleOrAllPhase;
+import io.openems.edge.common.type.Phase.SinglePhase;
+import io.openems.edge.common.type.TypeUtils;
+import io.openems.edge.ess.api.AsymmetricEss;
+import io.openems.edge.ess.api.HybridEss;
+import io.openems.edge.ess.api.ManagedAsymmetricEss;
+import io.openems.edge.ess.api.ManagedSinglePhaseEss;
+import io.openems.edge.ess.api.ManagedSymmetricEss;
+import io.openems.edge.ess.api.SinglePhaseEss;
+import io.openems.edge.ess.api.SymmetricEss;
+import io.openems.edge.ess.generic.common.CycleProvider;
+import io.openems.edge.ess.power.api.Power;
+import io.openems.edge.solaredge.charger.SolarEdgeCharger;
+import io.openems.edge.timedata.api.Timedata;
+import io.openems.edge.timedata.api.TimedataProvider;
+import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower;
+
+
+@Designate(ocd = Config.class, factory = true)
+@Component(//
+ name = "SolarEdge.ESS", //
+ immediate = true, //
+ configurationPolicy = ConfigurationPolicy.REQUIRE //
+)
+@EventTopics({ //
+ EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, //
+ EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE //
+})
+public class SolarEdgeEssImpl extends AbstractSunSpecEss implements SolarEdgeEss, ManagedSinglePhaseEss, SinglePhaseEss,
+ ManagedAsymmetricEss, AsymmetricEss, ManagedSymmetricEss, SymmetricEss, HybridEss, ModbusComponent,
+ OpenemsComponent, EventHandler, ModbusSlave, TimedataProvider, CycleProvider {
+
+ private static final SunSpecModel S_101_WITHOUT_EVENTS =
+ FilteredSunSpecModel.withoutPoints(
+ DefaultSunSpecModel.S_101,
+ DefaultSunSpecModel.S101.EVT1,
+ DefaultSunSpecModel.S101.EVT2,
+ DefaultSunSpecModel.S101.EVT_VND1,
+ DefaultSunSpecModel.S101.EVT_VND2,
+ DefaultSunSpecModel.S101.EVT_VND3,
+ DefaultSunSpecModel.S101.EVT_VND4
+ );
+
+ private static final SunSpecModel S_102_WITHOUT_EVENTS =
+ FilteredSunSpecModel.withoutPoints(
+ DefaultSunSpecModel.S_102,
+ DefaultSunSpecModel.S102.EVT1,
+ DefaultSunSpecModel.S102.EVT2,
+ DefaultSunSpecModel.S102.EVT_VND1,
+ DefaultSunSpecModel.S102.EVT_VND2,
+ DefaultSunSpecModel.S102.EVT_VND3,
+ DefaultSunSpecModel.S102.EVT_VND4
+ );
+
+ private static final SunSpecModel S_103_WITHOUT_EVENTS =
+ FilteredSunSpecModel.withoutPoints(
+ DefaultSunSpecModel.S_103,
+ DefaultSunSpecModel.S103.EVT1,
+ DefaultSunSpecModel.S103.EVT2,
+ DefaultSunSpecModel.S103.EVT_VND1,
+ DefaultSunSpecModel.S103.EVT_VND2,
+ DefaultSunSpecModel.S103.EVT_VND3,
+ DefaultSunSpecModel.S103.EVT_VND4
+ );
+
+ private static enum InverterType {
+ SINGLE_PHASE(S_101_WITHOUT_EVENTS), //
+ SPLIT_PHASE(S_102_WITHOUT_EVENTS), //
+ THREE_PHASE(S_103_WITHOUT_EVENTS);
+
+ private final List blocks;
+
+ private InverterType(SunSpecModel... blocks) {
+ this.blocks = Lists.newArrayList(blocks);
+ }
+ }
+
+ private static final int READ_FROM_MODBUS_BLOCK = 1;
+
+ private static final Map ACTIVE_MODELS = ImmutableMap.builder()
+ .put(DefaultSunSpecModel.S_1, Priority.LOW) //
+ .put(S_101_WITHOUT_EVENTS, Priority.HIGH) // DefaultSunSpecModel without Events (Events are not supported by SolarEdge)
+ .put(S_102_WITHOUT_EVENTS, Priority.HIGH) // DefaultSunSpecModel without Events (Events are not supported by SolarEdge)
+ .put(S_103_WITHOUT_EVENTS, Priority.HIGH) // DefaultSunSpecModel without Events (Events are not supported by SolarEdge)
+ .build();
+
+ private final AllowedChargeDischargeHandler allowedChargeDischargeHandler = new AllowedChargeDischargeHandler(this);
+ private final ApplyPowerHandler applyPowerHandler = new ApplyPowerHandler();
+ private final SetPvExportLimitHandler setPvExportLimitHandler = new SetPvExportLimitHandler(this);
+
+ private AverageCalculator pvProductionAverageCalculator;
+
+ protected final Set chargers = new HashSet<>();
+
+ private final Logger log = LoggerFactory.getLogger(SolarEdgeEss.class);
+
+ private Config config;
+ private SinglePhase singlePhase = null;
+ private InverterType inverterType = null;
+
+ private final ElementToChannelConverter ignoreMinPower = IgnoreMinPowerConverter.from(this, DIRECT_1_TO_1);
+
+ private final CalculateEnergyFromPower calculateAcChargeEnergy = new CalculateEnergyFromPower(this, SymmetricEss.ChannelId.ACTIVE_CHARGE_ENERGY);
+ private final CalculateEnergyFromPower calculateAcDischargeEnergy = new CalculateEnergyFromPower(this, SymmetricEss.ChannelId.ACTIVE_DISCHARGE_ENERGY);
+ private final CalculateEnergyFromPower calculateDcChargeEnergy = new CalculateEnergyFromPower(this, HybridEss.ChannelId.DC_CHARGE_ENERGY);
+ private final CalculateEnergyFromPower calculateDcDischargeEnergy = new CalculateEnergyFromPower(this, HybridEss.ChannelId.DC_DISCHARGE_ENERGY);
+
+ @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY)
+ protected void setModbus(BridgeModbus modbus) {
+ super.setModbus(modbus);
+ }
+
+ @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL)
+ private volatile Timedata timedata = null;
+
+ @Reference
+ private Cycle cycle;
+
+ @Reference
+ private ConfigurationAdmin cm;
+
+ @Reference
+ private Power power;
+
+ @Reference
+ private Sum sum;
+
+ @Reference
+ private ComponentManager componentManager;
+
+ public SolarEdgeEssImpl() throws OpenemsNamedException {
+ super(//
+ ACTIVE_MODELS, //
+ OpenemsComponent.ChannelId.values(), //
+ ModbusComponent.ChannelId.values(), //
+ HybridEss.ChannelId.values(), //
+ SymmetricEss.ChannelId.values(), //
+ ManagedSymmetricEss.ChannelId.values(), //
+ AsymmetricEss.ChannelId.values(), //
+ ManagedAsymmetricEss.ChannelId.values(), //
+ SinglePhaseEss.ChannelId.values(), //
+ ManagedSinglePhaseEss.ChannelId.values(), //
+ SolarEdgeEss.ChannelId.values() //
+ );
+ }
+
+ @Activate
+ private void activate(ComponentContext context, Config config) throws OpenemsNamedException {
+ this.config = config;
+ if (super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm,
+ "Modbus", config.modbus_id(), READ_FROM_MODBUS_BLOCK)) {
+ return;
+ }
+
+ // Evaluate 'SinglePhase'
+ switch (config.phase()) {
+ case ALL -> this.singlePhase = null;
+ case L1 -> this.singlePhase = SinglePhase.L1;
+ case L2 -> this.singlePhase = SinglePhase.L2;
+ case L3 -> this.singlePhase = SinglePhase.L3;
+ }
+
+ // update filter for 'Controllers'
+ if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "Controllers", config.id())) {
+ return;
+ }
+
+ this.pvProductionAverageCalculator = new AverageCalculator(120 * 1000 / this.getCycleTime()); // 120s average
+
+ this._setGridMode(GridMode.ON_GRID);
+ this.addStaticModbusTasks(this.getModbusProtocol());
+ }
+
+ //@Override
+ protected void onSunSpecInitializationCompleted() {
+ this.logInfo(this.log, "SunSpec initialization finished. " + this.channels().size() + " Channels available.");
+
+ this.channel(SolarEdgeEss.ChannelId.WRONG_PHASE_CONFIGURED).setNextValue(
+ this.inverterType == InverterType.SINGLE_PHASE ? this.config.phase() == SingleOrAllPhase.ALL : this.config.phase() != SingleOrAllPhase.ALL);
+
+ this.mapFirstPointToChannel(//
+ SolarEdgeEss.ChannelId.SERIAL_NUMBER, //
+ DIRECT_1_TO_1, //
+ S1.SN);
+
+ this.mapFirstPointToChannel(//
+ SymmetricEss.ChannelId.ACTIVE_POWER, //
+ this.ignoreMinPower, //
+ S101.W, S102.W, S103.W);
+ this.mapFirstPointToChannel(//
+ SymmetricEss.ChannelId.REACTIVE_POWER, //
+ DIRECT_1_TO_1, //
+ S101.V_AR, S102.V_AR, S103.V_AR);
+ this.mapFirstPointToChannel(//
+ SolarEdgeEss.ChannelId.INVERTER_ACTIVE_DC_POWER, //
+ DIRECT_1_TO_1, //
+ S101.DCW, S102.DCW, S103.DCW);
+
+ // Individual Phases Power
+ switch (this.inverterType) {
+ case SINGLE_PHASE -> {
+ SolarEdgeEssImpl.calculateSinglePhaseFromActivePower(this, this.config.phase());
+ }
+ case SPLIT_PHASE, THREE_PHASE -> {
+ SolarEdgeEssImpl.calculatePhasesFromActivePower(this);
+ SolarEdgeEssImpl.calculatePhasesFromReactivePower(this);
+ }
+ }
+
+ // PV Production Energy
+ this.mapFirstPointToChannel(//
+ SolarEdgeEss.ChannelId.ACTIVE_PRODUCTION_ENERGY, //
+ DIRECT_1_TO_1, //
+ S101.WH, S102.WH, S103.WH);
+
+ // DC Voltage
+ this.mapFirstPointToChannel(//
+ SolarEdgeEss.ChannelId.VOLTAGE_DC, //
+ SCALE_FACTOR_3, // Convert V to mV
+ S101.DCV, S102.DCV, S103.DCV);
+ }
+
+ @Override
+ protected void addBlock(int startAddress, SunSpecModel model, Priority priority) {
+ super.addBlock(startAddress, model, priority);
+
+ // Evaluate the InverterType from this Block
+ Stream.of(InverterType.values()) //
+ .filter(type -> type.blocks.stream().anyMatch(t -> t.equals(model))) //
+ .findFirst() //
+ .ifPresent(type -> this.inverterType = type);
+ }
+
+ @Override
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ }
+
+ /**
+ * Adds static modbus tasks.
+ *
+ * @param protocol the {@link ModbusProtocol}
+ * @throws OpenemsException on error
+ */
+ private void addStaticModbusTasks(ModbusProtocol protocol) throws OpenemsException {
+
+ // StorEdge Control Block register
+ protocol.addTask(//
+ new FC3ReadRegistersTask(0xE004, Priority.LOW, //
+ m(SolarEdgeEss.ChannelId.STORAGE_CONTROL_MODE, new UnsignedWordElement(0xE004)),
+ m(SolarEdgeEss.ChannelId.STORAGE_AC_CHARGE_POLICY, new UnsignedWordElement(0xE005)),
+ m(SolarEdgeEss.ChannelId.STORAGE_AC_CHARGE_LIMIT,
+ new FloatDoublewordElement(0xE006).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.STORAGE_BACKUP_RESERVED_SETTING,
+ new FloatDoublewordElement(0xE008).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.STORAGE_CHARGE_DISCHARGE_DEFAULT_MODE, new UnsignedWordElement(0xE00A)),
+ m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_TIMEOUT,
+ new UnsignedDoublewordElement(0xE00B).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_MODE, new UnsignedWordElement(0xE00D)),
+ m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_CHARGE_LIMIT,
+ new FloatDoublewordElement(0xE00E).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_DISCHARGE_LIMIT,
+ new FloatDoublewordElement(0xE010).wordOrder(WordOrder.LSWMSW))));
+
+ // StorEdge Battery 1 Status and Information Block register (Task 1)
+ protocol.addTask(//
+ new FC3ReadRegistersTask(0xE144, Priority.HIGH, //
+ m(SolarEdgeEss.ChannelId.BATTERY1_MAX_CHARGE_CONTINUES_POWER, // feeds AllowedChargeDischargeHandler
+ new FloatDoublewordElement(0xE144).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.BATTERY1_MAX_DISCHARGE_CONTINUES_POWER, // feeds AllowedChargeDischargeHandler
+ new FloatDoublewordElement(0xE146).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.BATTERY1_MAX_CHARGE_PEAK_POWER, //
+ new FloatDoublewordElement(0xE148).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.BATTERY1_MAX_DISCHARGE_PEAK_POWER, //
+ new FloatDoublewordElement(0xE14A).wordOrder(WordOrder.LSWMSW)),
+ new DummyRegisterElement(0xE14C, 0xE16B), // Reserved
+ m(SolarEdgeEss.ChannelId.BATTERY1_AVG_TEMPERATURE, //
+ new FloatDoublewordElement(0xE16C).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.BATTERY1_MAX_TEMPERATURE, //
+ new FloatDoublewordElement(0xE16E).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.BATTERY1_ACTUAL_VOLTAGE, //
+ new FloatDoublewordElement(0xE170).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.BATTERY1_ACTUAL_CURRENT, // Instantaneous Battery Current (charge / discharge)
+ new FloatDoublewordElement(0xE172).wordOrder(WordOrder.LSWMSW), SCALE_FACTOR_3), // Convert A to mA
+ m(SolarEdgeEss.ChannelId.BATTERY1_ACTUAL_POWER, // Instantaneous Battery Power (charge / discharge)
+ new FloatDoublewordElement(0xE174).wordOrder(WordOrder.LSWMSW))));
+
+ // StorEdge Battery 1 Status and Information Block register (Task 2)
+ protocol.addTask(//
+ new FC3ReadRegistersTask(0xE176, Priority.LOW,
+ // Active Charge / Discharge energy valid for SolarEdge SE4000
+ // For SolarEdge SE10K-RWS only valid until the next day/loading
+ // cycle (not clear or verified)
+ m(SolarEdgeEss.ChannelId.BATTERY1_LIFETIME_EXPORT_ENERGY, //
+ new UnsignedQuadruplewordElement(0xE176).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.BATTERY1_LIFETIME_IMPORT_ENERGY, //
+ new UnsignedQuadruplewordElement(0xE17A).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.BATTERY1_MAX_CAPACITY, //
+ new FloatDoublewordElement(0xE17E).wordOrder(WordOrder.LSWMSW)),
+ m(SymmetricEss.ChannelId.CAPACITY, // Available capacity or "real" capacity
+ new FloatDoublewordElement(0xE180).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.SOH, //
+ new FloatDoublewordElement(0xE182).wordOrder(WordOrder.LSWMSW)),
+ m(SymmetricEss.ChannelId.SOC, //
+ new FloatDoublewordElement(0xE184).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.BATTERY1_STATUS, //
+ new UnsignedDoublewordElement(0xE186).wordOrder(WordOrder.LSWMSW))));
+
+ // Smart Meter register
+ protocol.addTask(//
+ new FC3ReadRegistersTask(40121, Priority.LOW, //
+ m(SolarEdgeEss.ChannelId.METER_COMMUNICATE_STATUS, new UnsignedWordElement(40121))));
+
+ // Power Control Block Register
+ protocol.addTask(//
+ new FC3ReadRegistersTask(0xF140, Priority.LOW, //
+ m(SolarEdgeEss.ChannelId.INVERTER_POWER_LIMIT, // Power Reduction (e.g. 70%)
+ new FloatDoublewordElement(0xF140).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.ADVANCED_PWR_CONTROL_EN, // AdvacedPwrControlEn
+ new SignedDoublewordElement(0xF142).wordOrder(WordOrder.LSWMSW))));
+
+ // Enhanced Power Control Block register
+ protocol.addTask(//
+ new FC3ReadRegistersTask(0xF304, Priority.LOW, //
+ m(SolarEdgeEss.ChannelId.INVERTER_MAX_APPARENT_POWER,
+ new FloatDoublewordElement(0xF304).wordOrder(WordOrder.LSWMSW))));
+
+ // Export Limit Control Block register
+ protocol.addTask(//
+ new FC3ReadRegistersTask(0xE000, Priority.LOW, //
+ m(SolarEdgeEss.ChannelId.EXPORT_CONTROL_MODE, new UnsignedWordElement(0xE000)),
+ m(SolarEdgeEss.ChannelId.EXPORT_CONTROL_LIMIT_MODE, new UnsignedWordElement(0xE001)),
+ m(SolarEdgeEss.ChannelId.EXPORT_CONTROL_SITE_LIMIT, new FloatDoublewordElement(0xE002).wordOrder(WordOrder.LSWMSW))));
+
+ // StorEdge Control Block register
+ protocol.addTask(//
+ new FC16WriteRegistersTask(0xE00B,
+ m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_TIMEOUT,
+ new UnsignedDoublewordElement(0xE00B).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_MODE,
+ new UnsignedWordElement(0xE00D)),
+ m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_CHARGE_LIMIT,
+ new FloatDoublewordElement(0xE00E).wordOrder(WordOrder.LSWMSW)),
+ m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_DISCHARGE_LIMIT,
+ new FloatDoublewordElement(0xE010).wordOrder(WordOrder.LSWMSW))));
+
+ // Export Limit Control Block register
+ protocol.addTask(//
+ new FC16WriteRegistersTask(0xE002,
+ m(SolarEdgeEss.ChannelId.EXPORT_CONTROL_SITE_LIMIT,
+ new FloatDoublewordElement(0xE002).wordOrder(WordOrder.LSWMSW))));
+
+ }
+
+ @Override
+ public void applyPower(int activePower, int reactivePower) throws OpenemsNamedException {
+ // Apply Power Set-Point
+ this.applyPowerHandler.apply(this, activePower, this.config.controlMode(), this.sum.getGridActivePower(),
+ this.getActivePower(), this.power.isPidEnabled());
+ }
+
+ @Override
+ public void applyPower(int activePowerL1, int reactivePowerL1, int activePowerL2, int reactivePowerL2,
+ int activePowerL3, int reactivePowerL3) throws OpenemsNamedException {
+ if (this.config.phase() == SingleOrAllPhase.ALL) {
+ return;
+ }
+
+ ManagedSinglePhaseEss.super.applyPower(activePowerL1, reactivePowerL1, activePowerL2, reactivePowerL2,
+ activePowerL3, reactivePowerL3);
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ if (!this.isEnabled()) {
+ return;
+ }
+
+ switch (event.getTopic()) {
+ case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> {
+ this.calculateAndSetActualPvPower();
+ this.allowedChargeDischargeHandler.accept(this.componentManager);
+ this.updateMaxApparentPowerChannel();
+ this.updateDcDischargePowerChannel();
+ this.updateEnergyChannels();
+ }
+ case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE -> {
+ this.applyPvExportLimit();
+ }
+ }
+ }
+
+ /**
+ * Calculates Actual PV Power out of Total DC Power and Battery Charge/Discharge Power.
+ */
+ public void calculateAndSetActualPvPower() {
+ try {
+ final IntegerReadChannel activeDcPowerChannel = this.channel(SolarEdgeEss.ChannelId.INVERTER_ACTIVE_DC_POWER);
+ final IntegerReadChannel batteryPowerChannel = this.channel(SolarEdgeEss.ChannelId.BATTERY1_ACTUAL_POWER);
+
+ /*
+ * Get the DC Power value from 1 second ago
+ *
+ * Since the inverter has to retrieve the battery power value from the SESTI interface, this is outdated.
+ * For the PV production calculation to be accurate, it is assumed that the value is received with a 1-second delay.
+ * Therefore, we must add the battery power to the DCW value from 1 second ago to obtain the PV power.
+ *
+ */
+ var cycleTimeSeconds = Math.max(1, this.getCycleTime() / 1000); // minimum 1 second
+ var retrievePastSeconds = Math.min(cycleTimeSeconds, 1) * 2; // twice the cycle time or 2*1 second
+ var pastActiveDcPowerValues = activeDcPowerChannel.getPastValues()
+ .tailMap(LocalDateTime.now(this.componentManager.getClock()).minusSeconds(retrievePastSeconds), true); //
+
+ LocalDateTime target = batteryPowerChannel.getNextValue().getTimestamp().minusSeconds(1);
+ int dcPower = this.findCloseRecord(pastActiveDcPowerValues.values(), target).getOrError();
+
+ // Calculate the actual PV power and add it to the average values of the calculator
+ int lastPvPower = dcPower + batteryPowerChannel.getNextValue().getOrError();
+ this.pvProductionAverageCalculator.addValue(lastPvPower > 0 ? lastPvPower : 0);
+
+ // Get the actual PV power as an average
+ int pvProduction = ignoreImpossibleMinPower(this.pvProductionAverageCalculator.getAverage());
+
+ // Write the PV power into the actual power channel of our DC charger
+ for (SolarEdgeCharger charger : this.chargers) {
+ charger.getActualPowerChannel().setNextValue(pvProduction);
+ }
+ } catch (Exception e) {
+ return;
+ }
+ }
+
+ /**
+ * Find record in list closest to dateTime.
+ *
+ * @param list the list to search in
+ * @param dateTime the dateTime to which the searched record should be closest
+ * @return listEntry
+ */
+ public Value findCloseRecord(Collection> list, LocalDateTime dateTime) {
+ if (list == null || list.isEmpty()) {
+ return null;
+ }
+
+ return list.stream()
+ .min(Comparator.comparingLong(d -> Math.abs(MILLIS.between(dateTime, d.getTimestamp()))))
+ .orElse(null);
+ }
+
+ /**
+ * Ignore impossible minimum power.
+ *
+ *
+ * Even if there is no real power from PV, the Inverter Power Channel
+ * could remain on minimum power values. These values are ignored.
+ *
+ * @param pvProduction the pvProduction
+ * @return pvProduction or zero
+ */
+ protected static Integer ignoreImpossibleMinPower(Integer pvProduction) {
+ if (pvProduction == null) {
+ return pvProduction;
+ }
+
+ return pvProduction < 50 /* W */ ? 0 : pvProduction;
+ }
+
+ /**
+ * Send PV Export Site Limit to Inverter.
+ */
+ public void applyPvExportLimit() {
+ // Get ActiveExportPowerLimit that should be applied
+ var activeExportPowerLimitChannel = (IntegerWriteChannel) this
+ .channel(SolarEdgeEss.ChannelId.ACTIVE_EXPORT_POWER_LIMIT);
+ var activeExportPowerLimitOpt = activeExportPowerLimitChannel.getNextWriteValueAndReset();
+
+ // Set warning if pvExportLimit mode is disabled but a PV export limit was requested
+ this.channel(SolarEdgeEss.ChannelId.DISABLED_PV_EXPORT_LIMIT_FAILED)
+ .setNextValue(!this.config.pvExportLimit() && activeExportPowerLimitOpt.isPresent());
+
+ // If pvExportLimit mode is disabled: stop here
+ if (!this.config.pvExportLimit()) {
+ return;
+ }
+
+ try {
+ this.setPvExportLimitHandler.accept(activeExportPowerLimitOpt);
+
+ this.channel(SolarEdgeEss.ChannelId.PV_EXPORT_LIMIT_FAILED).setNextValue(false);
+ } catch (OpenemsNamedException e) {
+ this.channel(SolarEdgeEss.ChannelId.PV_EXPORT_LIMIT_FAILED).setNextValue(true);
+ }
+ }
+
+ @Override
+ public String debugLog() {
+ return "SoC:" + this.getSoc().asString() //
+ + "|L:" + this.getActivePower().asString() //
+ + "|Allowed:" + this.getAllowedChargePower().asStringWithoutUnit() + ";" //
+ + this.getAllowedDischargePower().asString() //
+ + "|" + this.getGridModeChannel().value().asOptionString();
+ }
+
+ @Override
+ public Power getPower() {
+ return this.power;
+ }
+
+ @Override
+ public int getPowerPrecision() {
+ return 1;
+ }
+
+ @Override
+ public SinglePhase getPhase() {
+ return this.singlePhase;
+ }
+
+ @Override
+ public Timedata getTimedata() {
+ return this.timedata;
+ }
+
+ @Override
+ public Integer getSurplusPower() {
+ // TODO logic is insufficient
+ if (this.getSoc().orElse(0) < 99) {
+ return null;
+ }
+ var productionPower = this.getPvProduction();
+ if (productionPower == null || productionPower < 100) {
+ return null;
+ }
+ return productionPower;
+ //return productionPower + 200 /* discharge more than PV production to avoid PV curtail */;
+ }
+
+ @Override
+ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) {
+ return new ModbusSlaveTable(//
+ OpenemsComponent.getModbusSlaveNatureTable(accessMode), //
+ SymmetricEss.getModbusSlaveNatureTable(accessMode), //
+ HybridEss.getModbusSlaveNatureTable(accessMode), //
+ this.getModbusSlaveNatureTable(accessMode)
+ );
+ }
+
+ @Override
+ public void addCharger(SolarEdgeCharger charger) {
+ this.chargers.add(charger);
+ }
+
+ @Override
+ public void removeCharger(SolarEdgeCharger charger) {
+ this.chargers.remove(charger);
+ }
+
+ @Override
+ public String getModbusBridgeId() {
+ return this.config.modbus_id();
+ }
+
+ @Override
+ protected void logInfo(Logger log, String message) {
+ super.logInfo(log, message);
+ }
+
+ /**
+ * Gets the PV production from chargers ACTUAL_POWER. Returns null if the PV
+ * production is not available.
+ *
+ * @return production power
+ *
+ */
+ public Integer getPvProduction() {
+ Integer productionPower = null;
+ for (SolarEdgeCharger charger : this.chargers) {
+ productionPower = TypeUtils.sum(productionPower, charger.getActualPower().get());
+ }
+ return productionPower;
+ }
+
+ protected void updateMaxApparentPowerChannel() {
+
+ /*
+ * Fill MaxApparentPower Channel
+ */
+ FloatReadChannel inverterMaxApparentPowerChannel = this.channel(SolarEdgeEss.ChannelId.INVERTER_MAX_APPARENT_POWER);
+ var inverterMaxApparentPower = inverterMaxApparentPowerChannel.value();
+ var inverterPowerLimit = this.getInverterPowerLimit();
+ if (inverterMaxApparentPower != null && inverterPowerLimit != null) {
+ this._setMaxApparentPower(Math.round(TypeUtils.multiply(inverterMaxApparentPower.get(), inverterPowerLimit.get(), 0.01f)));
+ }
+ }
+
+ protected void updateDcDischargePowerChannel() {
+ try {
+ /*
+ * Fill DcDischargePower Channel
+ * Reverse calculation from PV production as battery power values are outdated,
+ * because the inverter has to retrieve the value from the SESTI interface.
+ */
+ final IntegerReadChannel activeDcPowerChannel = this.channel(SolarEdgeEss.ChannelId.INVERTER_ACTIVE_DC_POWER);
+
+ // PV-Production
+ Integer pvProduction = 0;
+ for (SolarEdgeCharger charger : this.chargers) {
+ pvProduction = TypeUtils.sum(pvProduction, charger.getActualPowerChannel().getNextValue().getOrError());
+ }
+
+ var dcBatteryActualPower = activeDcPowerChannel.getNextValue().getOrError() - pvProduction;
+ this._setDcDischargePower(Math.abs(dcBatteryActualPower) > 50 ? dcBatteryActualPower : 0);
+ } catch (Exception e) {
+ return;
+ }
+ }
+
+ protected void updateEnergyChannels() {
+
+ /*
+ * Calculate AC Energy
+ */
+ var acActivePower = this.getActivePower().get();
+ if (acActivePower == null) {
+ // Not available
+ this.calculateAcChargeEnergy.update(null);
+ this.calculateAcDischargeEnergy.update(null);
+ } else if (acActivePower > 0) {
+ // Discharge
+ this.calculateAcChargeEnergy.update(0);
+ this.calculateAcDischargeEnergy.update(acActivePower);
+ } else {
+ // Charge
+ this.calculateAcChargeEnergy.update(acActivePower * -1);
+ this.calculateAcDischargeEnergy.update(0);
+ }
+
+ /*
+ * Calculate DC Energy
+ */
+ var dcDischargePower = this.getDcDischargePower().get();
+ if (dcDischargePower == null) {
+ // Not available
+ this.calculateDcChargeEnergy.update(null);
+ this.calculateDcDischargeEnergy.update(null);
+ } else if (dcDischargePower > 0) {
+ // Discharge
+ this.calculateDcChargeEnergy.update(0);
+ this.calculateDcDischargeEnergy.update(dcDischargePower);
+ } else {
+ // Charge
+ this.calculateDcChargeEnergy.update(dcDischargePower * -1);
+ this.calculateDcDischargeEnergy.update(0);
+ }
+ }
+
+ @Override
+ public int getCycleTime() {
+ return this.cycle != null ? this.cycle.getCycleTime() : DEFAULT_CYCLE_TIME;
+ }
+
+ /**
+ * Sets the correct value for ACTIVE_POWER_L1, ACTIVE_POWER_L2 or ACTIVE_POWER_L3-Channel from ACTIVE_POWER.
+ * @param solarEdge the SolarEdgeEssImpl instance
+ * @param phase the phase to which the inverter is connected
+ */
+ public static void calculateSinglePhaseFromActivePower(SolarEdgeEssImpl solarEdge, SingleOrAllPhase phase) {
+ solarEdge.getActivePowerChannel().onSetNextValue(value -> {
+ solarEdge.getActivePowerL1Channel().setNextValue(phase == SingleOrAllPhase.L1 || phase == SingleOrAllPhase.ALL ? value : null); // Fallback to L1 on wrong configuration
+ solarEdge.getActivePowerL2Channel().setNextValue(phase == SingleOrAllPhase.L2 ? value : null);
+ solarEdge.getActivePowerL3Channel().setNextValue(phase == SingleOrAllPhase.L3 ? value : null);
+ });
+ }
+
+ /**
+ * Calculate the ACTIVE_POWER_L1, ACTIVE_POWER_L2 and ACTIVE_POWER_L3-Channels from ACTIVE_POWER by dividing by three.
+ * @param solarEdge the SolarEdgeEssImpl instance
+ */
+ public static void calculatePhasesFromActivePower(SolarEdgeEssImpl solarEdge) {
+ solarEdge.getActivePowerChannel().onSetNextValue(value -> {
+ var phase = TypeUtils.divide(value.get(), 3);
+ solarEdge.getActivePowerL1Channel().setNextValue(phase);
+ solarEdge.getActivePowerL2Channel().setNextValue(phase);
+ solarEdge.getActivePowerL3Channel().setNextValue(phase);
+ });
+ }
+
+ /**
+ * Calculate the REACTIVE_POWER_L1, REACTIVE_POWER_L2 and REACTIVE_POWER_L3-Channels from REACTIVE_POWER by dividing by three.
+ * @param solarEdge the SolarEdgeEssImpl instance
+ */
+ public static void calculatePhasesFromReactivePower(SolarEdgeEssImpl solarEdge) {
+ solarEdge.getReactivePowerChannel().onSetNextValue(value -> {
+ var phase = TypeUtils.divide(value.get(), 3);
+ solarEdge.getReactivePowerL1Channel().setNextValue(phase);
+ solarEdge.getReactivePowerL2Channel().setNextValue(phase);
+ solarEdge.getReactivePowerL3Channel().setNextValue(phase);
+ });
+ }
+
+}
diff --git a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/pvinverter/SolarEdgePvInverterImpl.java b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/pvinverter/SolarEdgePvInverterImpl.java
index 0e3c5dc8b77..c171e0edd3b 100644
--- a/io.openems.edge.solaredge/src/io/openems/edge/solaredge/pvinverter/SolarEdgePvInverterImpl.java
+++ b/io.openems.edge.solaredge/src/io/openems/edge/solaredge/pvinverter/SolarEdgePvInverterImpl.java
@@ -24,6 +24,7 @@
import io.openems.edge.bridge.modbus.api.BridgeModbus;
import io.openems.edge.bridge.modbus.api.ModbusComponent;
import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel;
+import io.openems.edge.bridge.modbus.sunspec.FilteredSunSpecModel;
import io.openems.edge.bridge.modbus.sunspec.SunSpecModel;
import io.openems.edge.bridge.modbus.sunspec.pvinverter.AbstractSunSpecPvInverter;
import io.openems.edge.bridge.modbus.sunspec.pvinverter.SunSpecPvInverter;
@@ -51,25 +52,46 @@ public class SolarEdgePvInverterImpl extends AbstractSunSpecPvInverter
implements SolarEdgePvInverter, SunSpecPvInverter, ManagedSymmetricPvInverter, ElectricityMeter,
ModbusComponent, OpenemsComponent, EventHandler, ModbusSlave {
+ private static final SunSpecModel S_101_WITHOUT_EVENTS =
+ FilteredSunSpecModel.withoutPoints(
+ DefaultSunSpecModel.S_101,
+ DefaultSunSpecModel.S101.EVT1,
+ DefaultSunSpecModel.S101.EVT2,
+ DefaultSunSpecModel.S101.EVT_VND1,
+ DefaultSunSpecModel.S101.EVT_VND2,
+ DefaultSunSpecModel.S101.EVT_VND3,
+ DefaultSunSpecModel.S101.EVT_VND4
+ );
+
+ private static final SunSpecModel S_102_WITHOUT_EVENTS =
+ FilteredSunSpecModel.withoutPoints(
+ DefaultSunSpecModel.S_102,
+ DefaultSunSpecModel.S102.EVT1,
+ DefaultSunSpecModel.S102.EVT2,
+ DefaultSunSpecModel.S102.EVT_VND1,
+ DefaultSunSpecModel.S102.EVT_VND2,
+ DefaultSunSpecModel.S102.EVT_VND3,
+ DefaultSunSpecModel.S102.EVT_VND4
+ );
+
+ private static final SunSpecModel S_103_WITHOUT_EVENTS =
+ FilteredSunSpecModel.withoutPoints(
+ DefaultSunSpecModel.S_103,
+ DefaultSunSpecModel.S103.EVT1,
+ DefaultSunSpecModel.S103.EVT2,
+ DefaultSunSpecModel.S103.EVT_VND1,
+ DefaultSunSpecModel.S103.EVT_VND2,
+ DefaultSunSpecModel.S103.EVT_VND3,
+ DefaultSunSpecModel.S103.EVT_VND4
+ );
+
private static final int READ_FROM_MODBUS_BLOCK = 1;
private static final Map ACTIVE_MODELS = ImmutableMap.builder()
.put(DefaultSunSpecModel.S_1, Priority.LOW) //
- .put(DefaultSunSpecModel.S_101, Priority.LOW) //
- .put(DefaultSunSpecModel.S_102, Priority.LOW) //
- .put(DefaultSunSpecModel.S_103, Priority.LOW) //
- .put(DefaultSunSpecModel.S_111, Priority.LOW) //
- .put(DefaultSunSpecModel.S_112, Priority.LOW) //
- .put(DefaultSunSpecModel.S_113, Priority.LOW) //
- .put(DefaultSunSpecModel.S_120, Priority.LOW) //
- .put(DefaultSunSpecModel.S_121, Priority.LOW) //
- .put(DefaultSunSpecModel.S_122, Priority.LOW) //
- .put(DefaultSunSpecModel.S_123, Priority.LOW) //
- .put(DefaultSunSpecModel.S_124, Priority.LOW) //
- .put(DefaultSunSpecModel.S_125, Priority.LOW) //
- .put(DefaultSunSpecModel.S_127, Priority.LOW) //
- .put(DefaultSunSpecModel.S_128, Priority.LOW) //
- .put(DefaultSunSpecModel.S_145, Priority.LOW) //
+ .put(S_101_WITHOUT_EVENTS, Priority.LOW) //
+ .put(S_102_WITHOUT_EVENTS, Priority.LOW) //
+ .put(S_103_WITHOUT_EVENTS, Priority.LOW) //
.build();
@Reference
diff --git a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/charger/MyConfig.java b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/charger/MyConfig.java
new file mode 100644
index 00000000000..3e15e506555
--- /dev/null
+++ b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/charger/MyConfig.java
@@ -0,0 +1,62 @@
+package io.openems.edge.solaredge.charger;
+
+import io.openems.common.test.AbstractComponentConfig;
+import io.openems.common.utils.ConfigUtils;
+import io.openems.edge.common.type.Phase.SingleOrAllPhase;
+import io.openems.edge.solaredge.charger.Config;
+import io.openems.edge.solaredge.charger.MyConfig;
+import io.openems.edge.solaredge.charger.MyConfig.Builder;
+import io.openems.edge.solaredge.enums.ControlMode;
+
+@SuppressWarnings("all")
+public class MyConfig extends AbstractComponentConfig implements Config {
+
+ public static class Builder {
+ private String id;
+ private String essInverter;
+
+ private Builder() {
+ }
+
+ public Builder setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setEssInverterId(String essInverter) {
+ this.essInverter = essInverter;
+ return this;
+ }
+
+ public MyConfig build() {
+ return new MyConfig(this);
+ }
+ }
+
+ /**
+ * Create a Config builder.
+ *
+ * @return a {@link Builder}
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ private final Builder builder;
+
+ private MyConfig(Builder builder) {
+ super(Config.class, builder.id);
+ this.builder = builder;
+ }
+
+ @Override
+ public String essInverter_id() {
+ return this.builder.essInverter;
+ }
+
+ @Override
+ public String essInverter_target() {
+ return ConfigUtils.generateReferenceTargetFilter(this.id(), this.essInverter_id());
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/charger/SolarEdgeChargerImplTest.java b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/charger/SolarEdgeChargerImplTest.java
new file mode 100644
index 00000000000..e3d81b34d03
--- /dev/null
+++ b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/charger/SolarEdgeChargerImplTest.java
@@ -0,0 +1,21 @@
+package io.openems.edge.solaredge.charger;
+
+import org.junit.Test;
+
+import io.openems.common.test.DummyConfigurationAdmin;
+import io.openems.edge.common.test.ComponentTest;
+import io.openems.edge.solaredge.ess.SolarEdgeEssImpl;
+
+public class SolarEdgeChargerImplTest {
+
+ @Test
+ public void test() throws Exception {
+ new ComponentTest(new SolarEdgeChargerImpl()) //
+ .addReference("cm", new DummyConfigurationAdmin()) //
+ .addReference("essInverter", new SolarEdgeEssImpl())
+ .activate(MyConfig.create() //
+ .setId("charger0") //
+ .setEssInverterId("ess0") //
+ .build());
+ }
+}
diff --git a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/ess/MyConfig.java b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/ess/MyConfig.java
new file mode 100644
index 00000000000..47e1d8f6ec1
--- /dev/null
+++ b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/ess/MyConfig.java
@@ -0,0 +1,103 @@
+package io.openems.edge.solaredge.ess;
+
+import io.openems.common.utils.ConfigUtils;
+import io.openems.edge.common.type.Phase.SingleOrAllPhase;
+import io.openems.edge.solaredge.enums.ControlMode;
+import io.openems.common.test.AbstractComponentConfig;
+
+@SuppressWarnings("all")
+public class MyConfig extends AbstractComponentConfig implements Config {
+
+ protected static class Builder {
+ private String id;
+ private boolean pvExportLimit;
+ private String modbusId = null;
+ private int modbusUnitId;
+ private ControlMode controlMode;
+ private SingleOrAllPhase phase;
+
+ private Builder() {
+ }
+
+ public Builder setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setPvExportLimit(boolean pvExportLimit) {
+ this.pvExportLimit = pvExportLimit;
+ return this;
+ }
+
+ public Builder setModbusId(String modbusId) {
+ this.modbusId = modbusId;
+ return this;
+ }
+
+ public Builder setModbusUnitId(int modbusUnitId) {
+ this.modbusUnitId = modbusUnitId;
+ return this;
+ }
+
+ public Builder setPhase(SingleOrAllPhase phase) {
+ this.phase = phase;
+ return this;
+ }
+
+ public Builder setControlMode(ControlMode controlMode) {
+ this.controlMode = controlMode;
+ return this;
+ }
+
+ public MyConfig build() {
+ return new MyConfig(this);
+ }
+ }
+
+ /**
+ * Create a Config builder.
+ *
+ * @return a {@link Builder}
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ private final Builder builder;
+
+ private MyConfig(Builder builder) {
+ super(Config.class, builder.id);
+ this.builder = builder;
+ }
+
+ @Override
+ public boolean pvExportLimit() {
+ return this.builder.pvExportLimit;
+ }
+
+ @Override
+ public String modbus_id() {
+ return this.builder.modbusId;
+ }
+
+ @Override
+ public String Modbus_target() {
+ return ConfigUtils.generateReferenceTargetFilter(this.id(), this.modbus_id());
+ }
+
+ @Override
+ public int modbusUnitId() {
+ return this.builder.modbusUnitId;
+ }
+
+ @Override
+ public ControlMode controlMode() {
+ return this.builder.controlMode;
+ }
+
+ @Override
+ public SingleOrAllPhase phase() {
+ return this.builder.phase;
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.solaredge/test/io/openems/edge/solaredge/ess/SolarEdgeEssImplTest.java b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/ess/SolarEdgeEssImplTest.java
new file mode 100644
index 00000000000..85a8fc827be
--- /dev/null
+++ b/io.openems.edge.solaredge/test/io/openems/edge/solaredge/ess/SolarEdgeEssImplTest.java
@@ -0,0 +1,45 @@
+package io.openems.edge.solaredge.ess;
+
+import org.junit.Test;
+
+import io.openems.common.test.DummyConfigurationAdmin;
+import io.openems.edge.bridge.modbus.test.DummyModbusBridge;
+import io.openems.edge.common.test.AbstractComponentTest.TestCase;
+import io.openems.edge.common.test.ComponentTest;
+import io.openems.edge.common.test.DummyComponentManager;
+import io.openems.edge.common.type.Phase.SingleOrAllPhase;
+import io.openems.edge.solaredge.charger.SolarEdgeChargerImpl;
+import io.openems.edge.solaredge.enums.ControlMode;
+
+public class SolarEdgeEssImplTest {
+
+ @Test
+ public void test() throws Exception {
+ var charger = new SolarEdgeChargerImpl();
+ new ComponentTest(charger) //
+ .addReference("cm", new DummyConfigurationAdmin()) //
+ .addReference("essInverter", new SolarEdgeEssImpl())
+ .activate(io.openems.edge.solaredge.charger.MyConfig.create() //
+ .setId("charger0") //
+ .setEssInverterId("ess0") //
+ .build());
+
+ var ess = new SolarEdgeEssImpl();
+ ess.addCharger(charger);
+ new ComponentTest(ess) //
+ .addReference("cm", new DummyConfigurationAdmin()) //
+ .addReference("setModbus", new DummyModbusBridge("modbus0")) //
+ .addReference("componentManager", new DummyComponentManager()) //
+ .addComponent(charger)
+ .activate(MyConfig.create() //
+ .setId("ess0") //
+ .setControlMode(ControlMode.SMART) //
+ .setModbusId("modbus0") //
+ .setModbusUnitId(1) //
+ .setPhase(SingleOrAllPhase.L1) //
+ .build()) //
+ .next(new TestCase()) //
+ .deactivate();
+ }
+
+}