-
Notifications
You must be signed in to change notification settings - Fork 684
Sungrow PV Inverter and ESS Implementation #3935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sfeilmeier
merged 18 commits into
OpenEMS:develop
from
opernikus-common:feature/Sungrow
Sep 19, 2026
+2,255
−25
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
519c1f0
Implementation of Sungrow components
tsicking a824192
Fixed merge problems
tsicking 94cb8d6
Added JUnit tests
tsicking 3e1f01f
Added interfaces for grid meter and DC charger
tsicking 8a98719
Added Sungrow to EdgeApp
tsicking 3d8c8d1
Prepare commit
tsicking cbffee5
Merge branch 'develop' into feature/Sungrow
tsicking 13fb2c6
Removed duplicated entry
tsicking b76f154
Implemented getSurplusPower()
tsicking db25878
Merge remote-tracking branch 'origin/develop' into feature/Sungrow
sfeilmeier 0ed05dc
Apply tools/prepare-commit.sh
sfeilmeier 836613c
Removed default settings
tsicking 7aa9098
Use ReferenceConfigurationPlugin
tsicking b6c33f1
Merge remote-tracking branch 'origin/develop' into feature/Sungrow
sfeilmeier 18fa989
Cleanup
sfeilmeier 7053559
Review
sfeilmeier ccde13b
Add author and docs
sfeilmeier eae03b8
Add professional support link
sfeilmeier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <classpath> | ||
| <classpathentry kind="con" path="aQute.bnd.classpath.container"/> | ||
| <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21"/> | ||
| <classpathentry kind="src" output="bin" path="src"/> | ||
| <classpathentry kind="src" output="bin_test" path="test"> | ||
| <attributes> | ||
| <attribute name="test" value="true"/> | ||
| </attributes> | ||
| </classpathentry> | ||
| <classpathentry kind="output" path="bin"/> | ||
| </classpath> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| /bin_test/ | ||
| /generated/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <projectDescription> | ||
| <name>io.openems.edge.sungrow</name> | ||
| <comment></comment> | ||
| <projects> | ||
| </projects> | ||
| <buildSpec> | ||
| <buildCommand> | ||
| <name>org.eclipse.jdt.core.javabuilder</name> | ||
| <arguments> | ||
| </arguments> | ||
| </buildCommand> | ||
| <buildCommand> | ||
| <name>bndtools.core.bndbuilder</name> | ||
| <arguments> | ||
| </arguments> | ||
| </buildCommand> | ||
| </buildSpec> | ||
| <natures> | ||
| <nature>org.eclipse.jdt.core.javanature</nature> | ||
| <nature>bndtools.core.bndnature</nature> | ||
| </natures> | ||
| </projectDescription> |
2 changes: 2 additions & 0 deletions
2
io.openems.edge.sungrow/.settings/org.eclipse.core.resources.prefs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| eclipse.preferences.version=1 | ||
| encoding/<project>=UTF-8 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| Bundle-Name: OpenEMS Edge Sungrow | ||
| Bundle-Vendor: opernikus GmbH, OpenEMS Association e.V. | ||
| Bundle-License: https://opensource.org/licenses/EPL-2.0 | ||
| Bundle-Version: 1.0.0.${tstamp} | ||
|
|
||
| -buildpath: \ | ||
| ${buildpath},\ | ||
| io.openems.common,\ | ||
| io.openems.edge.bridge.modbus,\ | ||
| io.openems.edge.common,\ | ||
| io.openems.edge.ess.api,\ | ||
| io.openems.edge.meter.api,\ | ||
| io.openems.edge.pvinverter.api,\ | ||
| io.openems.edge.timedata.api,\ | ||
| io.openems.j2mod,\ | ||
|
|
||
| -testpath: \ | ||
| ${testpath} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| = Sungrow Energy Storage System and PV-Inverter | ||
|
|
||
| Support for | ||
|
|
||
| - Sungrow SH10RT Hybrid Ess (`Ess.Sungrow`); including | ||
| - DC-Chargers (`Ess.Sungrow.DcCharger`) | ||
| - Grid-Meter (`Ess.Sungrow.Gridmeter`) | ||
|
|
||
| - Sungrow PV-Inverter | ||
|
|
||
| If you need professional support with this implementation, please get in touch with opernikus GmbH (https://opernikus.de) | ||
|
|
||
| https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.sungrow[Source Code icon:github[]] |
25 changes: 25 additions & 0 deletions
25
io.openems.edge.sungrow/src/io/openems/edge/sungrow/dccharger/Config.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package io.openems.edge.sungrow.dccharger; | ||
|
|
||
| import org.osgi.service.metatype.annotations.AttributeDefinition; | ||
| import org.osgi.service.metatype.annotations.ObjectClassDefinition; | ||
|
|
||
| @ObjectClassDefinition(// | ||
| name = "ESS Sungrow DC charger", // | ||
| description = "Implements DC charger from Sungrow Hybrid ESS.") | ||
| @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 = "ESS-ID", description = "ID of the Sungrow Ess Component.") | ||
| String ess_id() default "ess0"; | ||
|
|
||
| String webconsole_configurationFactory_nameHint() default "ESS Sungrow DC charger [{id}]"; | ||
|
|
||
| } |
23 changes: 23 additions & 0 deletions
23
io.openems.edge.sungrow/src/io/openems/edge/sungrow/dccharger/SungrowDcCharger.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package io.openems.edge.sungrow.dccharger; | ||
|
|
||
| import io.openems.edge.common.channel.Doc; | ||
| import io.openems.edge.common.component.OpenemsComponent; | ||
|
|
||
| public interface SungrowDcCharger extends 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; | ||
| } | ||
| } | ||
|
|
||
| } |
120 changes: 120 additions & 0 deletions
120
io.openems.edge.sungrow/src/io/openems/edge/sungrow/dccharger/SungrowDcChargerImpl.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| package io.openems.edge.sungrow.dccharger; | ||
|
|
||
| import static io.openems.edge.common.event.EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE; | ||
| import static org.osgi.service.component.annotations.ReferenceCardinality.MANDATORY; | ||
| import static org.osgi.service.component.annotations.ReferenceCardinality.OPTIONAL; | ||
| import static org.osgi.service.component.annotations.ReferencePolicy.DYNAMIC; | ||
| import static org.osgi.service.component.annotations.ReferencePolicy.STATIC; | ||
| import static org.osgi.service.component.annotations.ReferencePolicyOption.GREEDY; | ||
|
|
||
| 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.event.Event; | ||
| import org.osgi.service.event.EventHandler; | ||
| import org.osgi.service.event.propertytypes.EventTopics; | ||
| import org.osgi.service.metatype.annotations.Designate; | ||
|
|
||
| import io.openems.common.exceptions.OpenemsException; | ||
| import io.openems.common.referencetarget.GenerateTargetsFromReferences; | ||
| import io.openems.edge.common.component.AbstractOpenemsComponent; | ||
| import io.openems.edge.common.component.OpenemsComponent; | ||
| import io.openems.edge.ess.dccharger.api.EssDcCharger; | ||
| import io.openems.edge.sungrow.ess.EssSungrow; | ||
| 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 = "Ess.Sungrow.DcCharger", // | ||
| immediate = true, // | ||
| configurationPolicy = ConfigurationPolicy.REQUIRE // | ||
| ) | ||
| @EventTopics({ // | ||
| TOPIC_CYCLE_AFTER_PROCESS_IMAGE }) | ||
| @GenerateTargetsFromReferences("ess") | ||
| public class SungrowDcChargerImpl extends AbstractOpenemsComponent | ||
| implements SungrowDcCharger, EssDcCharger, TimedataProvider, OpenemsComponent, EventHandler { | ||
|
|
||
| protected Config config = null; | ||
|
|
||
| @Reference | ||
| private ConfigurationAdmin cm; | ||
|
|
||
| @Reference(// | ||
| policy = STATIC, policyOption = GREEDY, cardinality = MANDATORY, // | ||
| target = "(&(id=${config.ess_id})(enabled=true))") | ||
| private EssSungrow ess; | ||
|
|
||
| @Reference(policy = DYNAMIC, policyOption = GREEDY, cardinality = OPTIONAL) | ||
| private volatile Timedata timedata = null; | ||
|
|
||
| private final CalculateEnergyFromPower calculateActualEnergy = new CalculateEnergyFromPower(this, | ||
| EssDcCharger.ChannelId.ACTUAL_ENERGY); | ||
|
|
||
| public SungrowDcChargerImpl() { | ||
| super(// | ||
| OpenemsComponent.ChannelId.values(), // | ||
| SungrowDcCharger.ChannelId.values(), // | ||
| EssDcCharger.ChannelId.values() // | ||
| ); | ||
| } | ||
|
|
||
| @Activate | ||
| private void activate(ComponentContext context, Config config) throws OpenemsException { | ||
| this.config = config; | ||
| super.activate(context, config.id(), config.alias(), config.enabled()); | ||
| this.mapChannelValues(); | ||
| } | ||
|
|
||
| @Deactivate | ||
| protected void deactivate() { | ||
| super.deactivate(); | ||
| } | ||
|
|
||
| private void mapChannelValues() { | ||
| this.ess.getTotalDcPowerChannel().onSetNextValue(value -> { | ||
| if (value.isDefined()) { | ||
| this._setActualPower(value.get()); | ||
| } | ||
| }); | ||
| this.ess.getMppt1VoltageChannel().onSetNextValue(value -> { | ||
| if (value.isDefined()) { | ||
| this._setVoltage(value.get()); | ||
| } | ||
| }); | ||
| this.ess.getMppt1CurrentChannel().onSetNextValue(value -> { | ||
| if (value.isDefined()) { | ||
| this._setCurrent(value.get()); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| @Override | ||
| public void handleEvent(Event event) { | ||
| if (!this.isEnabled()) { | ||
| return; | ||
| } | ||
| switch (event.getTopic()) { | ||
| case TOPIC_CYCLE_AFTER_PROCESS_IMAGE -> { | ||
| this.calculateActualEnergy.update(this.getActualPower().get()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public String debugLog() { | ||
| return "L:" + this.getActualPower().asString(); | ||
| } | ||
|
|
||
| @Override | ||
| public Timedata getTimedata() { | ||
| return this.timedata; | ||
| } | ||
|
|
||
| } | ||
74 changes: 74 additions & 0 deletions
74
io.openems.edge.sungrow/src/io/openems/edge/sungrow/ess/ApplyPowerHandler.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package io.openems.edge.sungrow.ess; | ||
|
|
||
| import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; | ||
| import io.openems.edge.common.channel.value.Value; | ||
| import io.openems.edge.sungrow.ess.enums.ChargeDischargeCommand; | ||
| import io.openems.edge.sungrow.ess.enums.ControlMode; | ||
| import io.openems.edge.sungrow.ess.enums.EmsMode; | ||
|
|
||
| /** | ||
| * Handler to apply the set active power to the Sungrow ESS depending on the | ||
| * configured {@link ControlMode}. | ||
| */ | ||
| public class ApplyPowerHandler { | ||
|
|
||
| /** | ||
| * Applies the desired active power setpoint by setting the appropiate EMS_MODE | ||
| * and Charge/Discharge power. | ||
| * | ||
| * @param parent the {@link EssSungrowImpl} | ||
| * @param setActivePower the active power setpoint | ||
| * @param controlMode the configured {@link ContolMode} | ||
| * @param gridActivePower the grid active power | ||
| * @throws OpenemsNamedException on write error | ||
| */ | ||
| public synchronized void apply(EssSungrowImpl parent, int setActivePower, ControlMode controlMode, | ||
| Value<Integer> gridActivePower) throws OpenemsNamedException { | ||
| parent.channel(EssSungrow.ChannelId.SMART_MODE_NOT_WORKING_WITH_PID_FILTER) // | ||
| .setNextValue(parent.power.isFilterEnabled() && controlMode.equals(ControlMode.SMART)); | ||
|
|
||
| var result = switch (controlMode) { | ||
| case INTERNAL -> handleInternalMode(); | ||
| case REMOTE -> handleRemoteMode(setActivePower); | ||
| case SMART -> handleSmartMode(setActivePower, parent.getActivePower(), gridActivePower); | ||
| default -> handleInternalMode(); | ||
| }; | ||
|
|
||
| parent.getEmsModeChannel().setNextWriteValue(result.emsMode); | ||
| if (result.setActivePower > 0) { | ||
| parent.getChargeDischargeCommandChannel().setNextWriteValue(ChargeDischargeCommand.DISCHARGE); | ||
| parent.getChargeDischargePowerChannel().setNextWriteValue(result.setActivePower); | ||
| } else { | ||
| parent.getChargeDischargeCommandChannel().setNextWriteValue(ChargeDischargeCommand.CHARGE); | ||
| parent.getChargeDischargePowerChannel().setNextWriteValue(-result.setActivePower); | ||
| } | ||
| } | ||
|
|
||
| private static record Result(EmsMode emsMode, int setActivePower) { | ||
| } | ||
|
|
||
| private static Result handleInternalMode() { | ||
| return new Result(EmsMode.SELF_CONSUMPTION, 0); | ||
| } | ||
|
|
||
| private static Result handleRemoteMode(int setActivePower) { | ||
| return new Result(EmsMode.EXTERNAL_EMS_MODE, setActivePower); | ||
| } | ||
|
|
||
| private static Result handleSmartMode(int setActivePower, Value<Integer> essActivePower, | ||
| Value<Integer> gridActivePower) { | ||
| // Fallback to internal mode if a value is undefined | ||
| if (!gridActivePower.isDefined() || !essActivePower.isDefined()) { | ||
| return handleInternalMode(); | ||
| } | ||
|
|
||
| // Is balancing to zero active? | ||
| var diffBalancing = setActivePower - (gridActivePower.get() + essActivePower.get()); | ||
| // avoid rounding errors | ||
| if (Math.abs(diffBalancing) <= 1) { | ||
| return handleInternalMode(); | ||
| } | ||
|
|
||
| return handleRemoteMode(setActivePower); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you not map those Registers directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the alternative would be to make this a
ModbusComponent, read the registers here and leave them out in the ESS implementation? This would mean that you send more modbus tasks per second but you can remove some channels from the ESS, right?I have no strong opinion which is better, but this was the first thing that came into my mind. I just had a look at the GoodWe implementations and saw that the single string chargers have their own modbus protocol, while the two string chargers don't.
Now, as I write, one more options comes into my mind. If the charger is referenced in the ESS and not the other way round, one could do something like the following in the modbus protocol:
Is that what you have in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I did not realize this is actually copying from
esscomponent. Then it's ok. Only flaw is, that you would have to remove the listener ondeactivate, otherwise the garbage collector will not be able to cleanup.