Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,13 @@
import io.openems.backend.metadata.odoo.odoo.OdooHandler;
import io.openems.backend.metadata.odoo.odoo.OdooUserRole;
import io.openems.backend.metadata.odoo.odoo.OdooUtils;
import io.openems.backend.metadata.odoo.odoo.http.OdooDeviceData;
import io.openems.backend.metadata.odoo.postgres.PostgresHandler;
import io.openems.backend.metrics.prometheus.DebugExecutor;
import io.openems.common.channel.Level;
import io.openems.common.event.EventBuilder;
import io.openems.common.event.EventReader;
import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.exceptions.OpenemsException;
import io.openems.common.exceptions.OpenemsRuntimeException;
import io.openems.common.jsonrpc.request.GetEdgesRequest.PaginationOptions;
import io.openems.common.jsonrpc.response.GetEdgesResponse.EdgeMetadata;
import io.openems.common.oem.OpenemsBackendOem;
Expand Down Expand Up @@ -752,28 +750,6 @@ public CompletableFuture<List<EdgeMetadata>> getPageDevice(//
});
}

private EdgeMetadata deviceDataToEdgeMetadata(User user, OdooDeviceData deviceData) {

// TODO remove cached edge
final var cachedEdge = this.getEdge(deviceData.name()).orElse(null);
if (cachedEdge == null) {
throw new OpenemsRuntimeException("Unable to find edge with id [" + deviceData.name() + "]");
}

return new EdgeMetadata(//
deviceData.name(), //
deviceData.comment(), //
deviceData.producttype(), //
cachedEdge.getVersion(), //
deviceData.role(), //
cachedEdge.isOnline(), //
deviceData.lastmessage(), //
deviceData.firstSetupProtocol(), //
deviceData.sumState(), //
deviceData.settings() //
);
}

@Override
public Role getUserRole(User user, String edgeId) {
final var userRoles = this.userRoles.computeIfAbsent(user.getId(), (userId) -> new ConcurrentHashMap<>());
Expand Down
2 changes: 2 additions & 0 deletions io.openems.edge.application/EdgeApp.bndrun
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
bnd.identity;id='io.openems.edge.simulator',\
bnd.identity;id='io.openems.edge.sma',\
bnd.identity;id='io.openems.edge.solaredge',\
bnd.identity;id='io.openems.edge.sungrow',\
bnd.identity;id='io.openems.edge.system.fenecon',\
bnd.identity;id='io.openems.edge.tesla.powerwall2',\
bnd.identity;id='io.openems.edge.timedata.influxdb',\
Expand Down Expand Up @@ -431,6 +432,7 @@
io.openems.edge.simulator;version=snapshot,\
io.openems.edge.sma;version=snapshot,\
io.openems.edge.solaredge;version=snapshot,\
io.openems.edge.sungrow;version=snapshot,\
io.openems.edge.system.fenecon;version=snapshot,\
io.openems.edge.tesla.powerwall2;version=snapshot,\
io.openems.edge.thermometer.api;version=snapshot,\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,6 @@ protected void validateOutputsReadValue(AbstractComponentTest<?, ?> act) throws
}
}

@SuppressWarnings("unchecked")
private Object resolveEnum(Channel<?> channel, Object got) {
if (channel.channelDoc() instanceof EnumDoc enumDoc) {
var intGot = TypeUtils.<Integer>getAsType(OpenemsType.INTEGER, got);
Expand Down
12 changes: 12 additions & 0 deletions io.openems.edge.sungrow/.classpath
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>
2 changes: 2 additions & 0 deletions io.openems.edge.sungrow/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin_test/
/generated/
23 changes: 23 additions & 0 deletions io.openems.edge.sungrow/.project
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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8
18 changes: 18 additions & 0 deletions io.openems.edge.sungrow/bnd.bnd
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}
13 changes: 13 additions & 0 deletions io.openems.edge.sungrow/readme.adoc
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[]]
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}]";

}
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;
}
}

}
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 -> {

Copy link
Copy Markdown
Contributor

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?

Copy link
Copy Markdown
Contributor Author

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:

m(new UnsignedDoubleWordElement(5016)).build() //
								.onUpdateCallback(value -> this.charger._setActualPower(value)), //

Is that what you have in mind?

Copy link
Copy Markdown
Contributor

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 ess component. Then it's ok. Only flaw is, that you would have to remove the listener on deactivate, otherwise the garbage collector will not be able to cleanup.

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;
}

}
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);
}
}
Loading
Loading