Skip to content
Open
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
2 changes: 1 addition & 1 deletion io.openems.edge.application/EdgeApp.bndrun
Original file line number Diff line number Diff line change
Expand Up @@ -503,4 +503,4 @@
org.owasp.encoder;version='[1.4.0,1.4.1)',\
reactive-streams;version='[1.0.4,1.0.5)',\
rrd4j;version='[3.10.0,3.10.1)',\
stax2-api;version='[4.3.0,4.3.1)'
stax2-api;version='[4.3.0,4.3.1)'
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package io.openems.edge.app.ess;

import java.util.List;
import java.util.Map;
import java.util.function.Function;

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.Reference;

import com.google.gson.JsonElement;

import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.function.ThrowingTriFunction;
import io.openems.common.session.Language;
import io.openems.common.types.EdgeConfig;
import io.openems.common.utils.JsonUtils;
import io.openems.edge.app.common.props.CommonProps;
import io.openems.edge.app.common.props.CommunicationProps;
import io.openems.edge.app.ess.SmaEssSunnyBoyStorage.Property;
import io.openems.edge.common.component.ComponentManager;
import io.openems.edge.core.appmanager.AbstractOpenemsApp;
import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps;
import io.openems.edge.core.appmanager.AppConfiguration;
import io.openems.edge.core.appmanager.AppDef;
import io.openems.edge.core.appmanager.ComponentUtil;
import io.openems.edge.core.appmanager.ConfigurationTarget;
import io.openems.edge.core.appmanager.Nameable;
import io.openems.edge.core.appmanager.OpenemsApp;
import io.openems.edge.core.appmanager.OpenemsAppCardinality;
import io.openems.edge.core.appmanager.OpenemsAppCategory;
import io.openems.edge.core.appmanager.Type;
import io.openems.edge.core.appmanager.Type.Parameter;
import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter;
import io.openems.edge.core.appmanager.dependency.Tasks;
import io.openems.edge.core.appmanager.formly.JsonFormlyUtil;
import io.openems.edge.core.appmanager.formly.enums.InputType;

/**
* Describes an App for the SMA Sunny Boy Storage 2.5 ESS.
*
* <pre>
{
"appId":"App.Ess.Sma.SunnyBoyStorage",
"alias":"SMA Sunny Boy Storage 2.5",
"instanceId": UUID,
"image": base64,
"properties":{
"ESS_ID": "ess0",
"MODBUS_ID": "modbus0",
"IP": "192.168.178.85",
"PORT": "502",
"MODBUS_UNIT_ID": "3",
"CAPACITY": "2000",
"READ_ONLY_MODE": "false"
},
"appDescriptor": {
"websiteUrl": {@link io.openems.edge.core.appmanager.AppDescriptor#getWebsiteUrl()}
}
}
* </pre>
*/
@Component(name = "App.Ess.Sma.SunnyBoyStorage")
public class SmaEssSunnyBoyStorage
extends AbstractOpenemsAppWithProps<SmaEssSunnyBoyStorage, Property, Parameter.BundleParameter>
implements OpenemsApp {

public static enum Property
implements Type<Property, SmaEssSunnyBoyStorage, Parameter.BundleParameter>, Nameable {

// Component-IDs
ESS_ID(AppDef.componentId("ess0")), //
MODBUS_ID(AppDef.componentId("modbus0")), //

// User-visible properties
ALIAS(CommonProps.alias()), //
IP(AppDef.copyOfGeneric(CommunicationProps.ip(), def -> def //
.setDefaultValue("192.168.178.85") //
.setRequired(true))), //
PORT(AppDef.copyOfGeneric(CommunicationProps.port(), def -> def //
.setRequired(true))), //
MODBUS_UNIT_ID(AppDef.copyOfGeneric(CommunicationProps.modbusUnitId(), def -> def //
.setDefaultValue(3) //
.setRequired(true))), //
CAPACITY(AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def //
.setLabel("Kapazität [Wh]") //
.setDescription("Nutzbare Netto-Kapazität der Batterie in Wh. SBS 2.5 = 2000 Wh") //
.setDefaultValue(2000) //
.setField(JsonFormlyUtil::buildInputFromNameable,
(app, prop, l, param, f) -> f.setInputType(InputType.NUMBER).setMin(0)))), //
READ_ONLY_MODE(AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def //
.setLabel("Nur-Lesen-Modus") //
.setDescription("Aktiviert den Nur-Lesen-Modus; keine Sollwerte werden an das Gerät geschrieben.") //
.setDefaultValue(false) //
.setField(JsonFormlyUtil::buildCheckboxFromNameable))), //
;

private final AppDef<? super SmaEssSunnyBoyStorage, ? super Property, ? super BundleParameter> def;

private Property(AppDef<? super SmaEssSunnyBoyStorage, ? super Property, ? super BundleParameter> def) {
this.def = def;
}

@Override
public Property self() {
return this;
}

@Override
public AppDef<? super SmaEssSunnyBoyStorage, ? super Property, ? super BundleParameter> def() {
return this.def;
}

@Override
public Function<GetParameterValues<SmaEssSunnyBoyStorage>, BundleParameter> getParamter() {
return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle);
}
}

@Activate
public SmaEssSunnyBoyStorage(@Reference ComponentManager componentManager, ComponentContext context,
@Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) {
super(componentManager, context, cm, componentUtil);
}

@Override
protected ThrowingTriFunction<ConfigurationTarget, Map<Property, JsonElement>, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() {
return (t, p, l) -> {
final var alias = this.getString(p, l, Property.ALIAS);
final var ip = this.getString(p, l, Property.IP);
final var port = this.getInt(p, Property.PORT);
final var modbusUnitId = this.getInt(p, Property.MODBUS_UNIT_ID);
final var capacity = this.getInt(p, Property.CAPACITY);
final var readOnlyMode = this.getBoolean(p, Property.READ_ONLY_MODE);

final var essId = this.getId(t, p, Property.ESS_ID);
final var modbusId = this.getId(t, p, Property.MODBUS_ID);

final var components = List.of(//
new EdgeConfig.Component(essId, alias, "Ess.Sma.SunnyBoyStorage", //
JsonUtils.buildJsonObject() //
.addProperty("modbus.id", modbusId) //
.addProperty("modbusUnitId", modbusUnitId) //
.addProperty("capacity", capacity) //
.addProperty("readOnlyMode", readOnlyMode) //
.build()), //
new EdgeConfig.Component(modbusId, alias, "Bridge.Modbus.Tcp", //
JsonUtils.buildJsonObject() //
.addProperty("ip", ip) //
.addProperty("port", port) //
.build()) //
);

return AppConfiguration.create() //
.addTask(Tasks.component(components)) //
.build();
};
}

@Override
public OpenemsAppCardinality getCardinality() {
return OpenemsAppCardinality.MULTIPLE;
}

@Override
public OpenemsAppCategory[] getCategories() {
return new OpenemsAppCategory[] { OpenemsAppCategory.ESS };
}

@Override
protected SmaEssSunnyBoyStorage getApp() {
return this;
}

@Override
protected Property[] propertyValues() {
return Property.values();
}
}
Empty file.
8 changes: 7 additions & 1 deletion io.openems.edge.sma/readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
Tested on
- SMA SunnyIsland 6.0H
- Sunny Island 4.4M
- SMA Sunny Boy Storage 2.5
- SMA STP 10.0 Hybrid ESS. (The implementation is split into Battery, Inverter and DC Charger. The inverter only works with that battery, but the implementation had to be split up as the devices use different Modbus Unit IDs.)

The Sunny Boy Storage implementation uses the SMA BMS external control interface
(CmpBMS). The control registers for BMS mode, min/max charge power,
min/max discharge power and grid power setpoint must be written within every
60-second window or the device reverts to autonomous mode.

== Meter

Tested on
Expand All @@ -24,4 +30,4 @@ Tested on
- https://www.sma.de/produkte/solar-wechselrichter/sunny-tripower-80-100.html[SMA SUNNY TRIPOWER 8.0 / 10.0 ]
- https://www.sma.de/produkte/solar-wechselrichter/sunny-tripower-15000tl-20000tl-25000tl.html[SMA SUNNY TRIPOWER 15000TL / 20000TL / 25000TL]

https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.sma[Source Code icon:github[]]
https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.sma[Source Code icon:github[]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.openems.edge.sma.ess.sunnyboystorage;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(//
name = "ESS SMA Sunny Boy Storage", //
description = "Implements the SMA Sunny Boy Storage 2.5 energy storage system via Modbus TCP.")
@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 = "Read-Only mode", description = "Enables Read-Only mode; no setpoints are written to the device")
boolean readOnlyMode() default false;

@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. SMA SBS default: 3")
int modbusUnitId() default 3;

@AttributeDefinition(name = "Capacity [Wh]", description = "Net usable capacity of the battery in Wh. SBS 2.5 = 2000 Wh usable")
int capacity() default 2000;

String webconsole_configurationFactory_nameHint() default "ESS SMA Sunny Boy Storage [{id}]";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package io.openems.edge.sma.ess.sunnyboystorage;

import io.openems.common.channel.AccessMode;
import io.openems.common.channel.Unit;
import io.openems.common.types.OpenemsType;
import io.openems.edge.common.channel.Doc;

public interface EssSmaSunnyBoyStorage {

public enum ChannelId implements io.openems.edge.common.channel.ChannelId {

/**
* Total energy delivered by the battery (charge + discharge combined).
*
* <ul>
* <li>Register 30513, uint64, FC3
* <li>Unit: Wh
* </ul>
*/
ENERGY_TOTAL(Doc.of(OpenemsType.LONG) //
.unit(Unit.CUMULATED_WATT_HOURS)), //

// --- Write channels (mapped to Modbus holding registers 40xxx via FC16) ---

/**
* BMS operating mode (CmpBMS register 40236).
*
* <ul>
* <li>Register 40236, uint32, FC16
* <li>2289 = Charge battery
* <li>2290 = Discharge battery
* <li>2424 = Presetting (self-consumption, internal BMS)
* </ul>
*/
BMS_MODE(Doc.of(OpenemsType.INTEGER) //
.accessMode(AccessMode.WRITE_ONLY)), //

/**
* Minimum charge power sent to the inverter (CmpBMS.BatChaMinW).
*
* <ul>
* <li>Register 40793, uint32, FC16
* <li>Unit: W. Set to 0 for normal operation, equal to MAX_CHARGE_POWER to
* force a fixed charge power.
* </ul>
*/
MIN_CHARGE_POWER(Doc.of(OpenemsType.INTEGER) //
.unit(Unit.WATT) //
.accessMode(AccessMode.WRITE_ONLY)), //

/**
* Maximum charge power limit sent to the inverter (CmpBMS.BatChaMaxW).
*
* <ul>
* <li>Register 40795, uint32, FC16
* <li>Unit: W, Range: 0..2500
* </ul>
*/
MAX_CHARGE_POWER(Doc.of(OpenemsType.INTEGER) //
.unit(Unit.WATT) //
.accessMode(AccessMode.WRITE_ONLY)), //

/**
* Minimum discharge power sent to the inverter (CmpBMS.BatDschMinW).
*
* <ul>
* <li>Register 40797, uint32, FC16
* <li>Unit: W. Typically 0 (no minimum discharge enforced).
* </ul>
*/
MIN_DISCHARGE_POWER(Doc.of(OpenemsType.INTEGER) //
.unit(Unit.WATT) //
.accessMode(AccessMode.WRITE_ONLY)), //

/**
* Maximum discharge power limit sent to the inverter (CmpBMS.BatDschMaxW).
*
* <ul>
* <li>Register 40799, uint32, FC16
* <li>Unit: W, Range: 0..2500
* </ul>
*/
MAX_DISCHARGE_POWER(Doc.of(OpenemsType.INTEGER) //
.unit(Unit.WATT) //
.accessMode(AccessMode.WRITE_ONLY)), //

/**
* Grid power setpoint (CmpBMS.GridWSpt).
*
* <p>
* SMA sign convention: positive = import from grid (charging), negative =
* export to grid (discharging). This is the inverse of the OpenEMS
* ACTIVE_POWER convention; write {@code -activePower}.
*
* <ul>
* <li>Register 40801, int32, FC16
* <li>Unit: W
* </ul>
*/
GRID_POWER_SETPOINT(Doc.of(OpenemsType.INTEGER) //
.unit(Unit.WATT) //
.accessMode(AccessMode.WRITE_ONLY)); //

private final Doc doc;

private ChannelId(Doc doc) {
this.doc = doc;
}

@Override
public Doc doc() {
return this.doc;
}
}
}
Loading