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. + * + *

+ */ + 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. + * + */ + STORAGE_CONTROL_MODE(Doc.of(SeControlMode.values()).accessMode(AccessMode.READ_ONLY)), + + /** + * Defines the AC charge policy for the storage system. + * + */ + 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. + * + * + */ + 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 + * + */ + 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. + * + */ + 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. + * + * + */ + BATTERY1_MAX_CHARGE_CONTINUES_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Battery 1 Max Discharge Continues Power. Varies with SoC. + * + * + */ + BATTERY1_MAX_DISCHARGE_CONTINUES_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Battery 1 Max Charge Peak Power. Varies with SoC. ????? + * + * + */ + 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. ????? + * + * + */ + BATTERY1_MAX_DISCHARGE_PEAK_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Battery 1 Average Temperature. + * + * + */ + BATTERY1_AVG_TEMPERATURE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.DEGREE_CELSIUS) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Battery 1 Max Temperature. + * + * + */ + BATTERY1_MAX_TEMPERATURE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.DEGREE_CELSIUS) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Battery 1 Actual Voltage. + * + * + */ + BATTERY1_ACTUAL_VOLTAGE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Battery 1 Actual Current to or from the battery. + * + * + */ + BATTERY1_ACTUAL_CURRENT(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Battery 1 Actual Charge/Discharge Power. + * + * + */ + 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! + * + * + */ + 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! + * + */ + BATTERY1_LIFETIME_IMPORT_ENERGY(Doc.of(OpenemsType.LONG) // + .unit(Unit.WATT_HOURS) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Battery 1 Max. Capacity. + * + * + */ + BATTERY1_MAX_CAPACITY(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT_HOURS) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Battery 1 State Of Health. + * + * + */ + 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"); // + * + */ + BATTERY1_STATUS(Doc.of(BatteryStatus.values()).accessMode(AccessMode.READ_ONLY)), + + /** + * Inverter Actual DC Power. + * + * + */ + INVERTER_ACTIVE_DC_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.HIGH)), + + /** + * Inverter Max Apparent Power. + * + * + */ + INVERTER_MAX_APPARENT_POWER(Doc.of(OpenemsType.FLOAT) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Power Control Fixed Power Limit. + * + * + */ + INVERTER_POWER_LIMIT(Doc.of(OpenemsType.FLOAT) // + .unit(Unit.PERCENT) // + .persistencePriority(PersistencePriority.LOW)), + + /** + * Advanced Power Control Enabled. + * + * + */ + ADVANCED_PWR_CONTROL_EN(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY)), + + /** + * Export Control Mode. + * + * + */ + EXPORT_CONTROL_MODE(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY)), + + /** + * Export Control Limit Mode. + * + * + */ + EXPORT_CONTROL_LIMIT_MODE(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY)), + + /** + * Export Control Site Limit. + * + * + */ + EXPORT_CONTROL_SITE_LIMIT(Doc.of(OpenemsType.FLOAT) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.HIGH) + .accessMode(AccessMode.READ_WRITE)), + + /** + * Active Production Energy. + * + * + */ + 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. + * + * + */ + 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(); + } + +}