Skip to content

Commit 8f5bee8

Browse files
Extend SunSpec PV-Inverter with support for Single-Phase inverters (OpenEMS#1854)
See discussion in OpenEMS Community: https://community.openems.io/t/sma-sunnyboy-1phasig/973/3 Co-authored-by: Michael Grill <[email protected]>
1 parent c1290bb commit 8f5bee8

File tree

9 files changed

+154
-25
lines changed

9 files changed

+154
-25
lines changed

io.openems.edge.pvinverter.fronius/src/io/openems/edge/pvinverter/fronius/FroniusPvInverter.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.openems.edge.meter.api.SymmetricMeter;
3535
import io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter;
3636
import io.openems.edge.pvinverter.sunspec.AbstractSunSpecPvInverter;
37+
import io.openems.edge.pvinverter.sunspec.Phase;
3738
import io.openems.edge.pvinverter.sunspec.SunSpecPvInverter;
3839

3940
@Designate(ocd = Config.class, factory = true)
@@ -85,7 +86,7 @@ protected void setModbus(BridgeModbus modbus) {
8586
@Activate
8687
void activate(ComponentContext context, Config config) throws OpenemsException {
8788
if (super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm,
88-
"Modbus", config.modbus_id(), READ_FROM_MODBUS_BLOCK)) {
89+
"Modbus", config.modbus_id(), READ_FROM_MODBUS_BLOCK, Phase.ALL)) {
8990
return;
9091
}
9192
}

io.openems.edge.pvinverter.kaco.blueplanet/src/io/openems/edge/pvinverter/kaco/blueplanet/KacoBlueplanet.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.openems.edge.meter.api.SymmetricMeter;
3535
import io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter;
3636
import io.openems.edge.pvinverter.sunspec.AbstractSunSpecPvInverter;
37+
import io.openems.edge.pvinverter.sunspec.Phase;
3738
import io.openems.edge.pvinverter.sunspec.SunSpecPvInverter;
3839

3940
@Designate(ocd = Config.class, factory = true)
@@ -96,7 +97,7 @@ protected void setModbus(BridgeModbus modbus) {
9697
@Activate
9798
void activate(ComponentContext context, Config config) throws OpenemsException {
9899
if (super.activate(context, config.id(), config.alias(), config.enabled(), UNIT_ID, this.cm, "Modbus",
99-
config.modbus_id(), READ_FROM_MODBUS_BLOCK)) {
100+
config.modbus_id(), READ_FROM_MODBUS_BLOCK, Phase.ALL)) {
100101
return;
101102
}
102103
}

io.openems.edge.pvinverter.kostal/src/io/openems/edge/pvinverter/kostal/KostalPvInverter.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.openems.edge.meter.api.SymmetricMeter;
3535
import io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter;
3636
import io.openems.edge.pvinverter.sunspec.AbstractSunSpecPvInverter;
37+
import io.openems.edge.pvinverter.sunspec.Phase;
3738
import io.openems.edge.pvinverter.sunspec.SunSpecPvInverter;
3839

3940
@Designate(ocd = Config.class, factory = true)
@@ -85,7 +86,7 @@ protected void setModbus(BridgeModbus modbus) {
8586
@Activate
8687
private void activate(ComponentContext context, Config config) throws OpenemsException {
8788
if (super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm,
88-
"Modbus", config.modbus_id(), READ_FROM_MODBUS_BLOCK)) {
89+
"Modbus", config.modbus_id(), READ_FROM_MODBUS_BLOCK, Phase.ALL)) {
8990
return;
9091
}
9192
}

io.openems.edge.pvinverter.sma/src/io/openems/edge/pvinverter/sma/Config.java

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import org.osgi.service.metatype.annotations.AttributeDefinition;
44
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
55

6+
import io.openems.edge.pvinverter.sunspec.Phase;
7+
68
@ObjectClassDefinition(name = "PV-Inverter SMA Sunny Tripower", //
79
description = "Implements the SMA Sunny Tripower PV inverter.")
810
@interface Config {
@@ -24,6 +26,9 @@
2426
+ "in the SMA web interface.")
2527
int modbusUnitId() default 126;
2628

29+
@AttributeDefinition(name = "Phase", description = "On which phase is the inverter connected?")
30+
Phase phase() default Phase.ALL;
31+
2732
@AttributeDefinition(name = "Modbus target filter", description = "This is auto-generated by 'Modbus-ID'.")
2833
String Modbus_target() default "(enabled=true)";
2934

io.openems.edge.pvinverter.sma/src/io/openems/edge/pvinverter/sma/SmaPvInverter.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable;
3232
import io.openems.edge.common.modbusslave.ModbusSlaveTable;
3333
import io.openems.edge.common.taskmanager.Priority;
34+
import io.openems.edge.meter.api.AsymmetricMeter;
3435
import io.openems.edge.meter.api.SymmetricMeter;
3536
import io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter;
3637
import io.openems.edge.pvinverter.sunspec.AbstractSunSpecPvInverter;
@@ -48,10 +49,11 @@
4849
EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE //
4950
})
5051
public class SmaPvInverter extends AbstractSunSpecPvInverter implements SunSpecPvInverter, ManagedSymmetricPvInverter,
51-
SymmetricMeter, ModbusComponent, OpenemsComponent, EventHandler, ModbusSlave {
52+
SymmetricMeter, ModbusComponent, AsymmetricMeter, OpenemsComponent, EventHandler, ModbusSlave {
5253

5354
private static final Map<SunSpecModel, Priority> ACTIVE_MODELS = ImmutableMap.<SunSpecModel, Priority>builder()
5455
.put(DefaultSunSpecModel.S_1, Priority.LOW) // from 40002
56+
.put(DefaultSunSpecModel.S_101, Priority.LOW) // from 40081
5557
.put(DefaultSunSpecModel.S_103, Priority.HIGH) // from 40185
5658
.put(DefaultSunSpecModel.S_120, Priority.LOW) // from 40237
5759
.put(DefaultSunSpecModel.S_121, Priority.LOW) // from 40265
@@ -83,6 +85,7 @@ public SmaPvInverter() throws OpenemsException {
8385
OpenemsComponent.ChannelId.values(), //
8486
ModbusComponent.ChannelId.values(), //
8587
SymmetricMeter.ChannelId.values(), //
88+
AsymmetricMeter.ChannelId.values(), //
8689
ManagedSymmetricPvInverter.ChannelId.values(), //
8790
SunSpecPvInverter.ChannelId.values() //
8891
);
@@ -97,7 +100,7 @@ protected void setModbus(BridgeModbus modbus) {
97100
@Activate
98101
void activate(ComponentContext context, Config config) throws OpenemsException {
99102
if (super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm,
100-
"Modbus", config.modbus_id(), READ_FROM_MODBUS_BLOCK)) {
103+
"Modbus", config.modbus_id(), READ_FROM_MODBUS_BLOCK, config.phase())) {
101104
return;
102105
}
103106
}

io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/AbstractSunSpecPvInverter.java

+124-18
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import org.slf4j.Logger;
1212
import org.slf4j.LoggerFactory;
1313

14+
import com.google.common.collect.Lists;
15+
1416
import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
1517
import io.openems.common.exceptions.OpenemsException;
1618
import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
@@ -22,16 +24,20 @@
2224
import io.openems.edge.common.component.OpenemsComponent;
2325
import io.openems.edge.common.event.EdgeEventConstants;
2426
import io.openems.edge.common.taskmanager.Priority;
27+
import io.openems.edge.meter.api.AsymmetricMeter;
2528
import io.openems.edge.meter.api.MeterType;
2629
import io.openems.edge.meter.api.SymmetricMeter;
2730
import io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter;
2831

29-
public abstract class AbstractSunSpecPvInverter extends AbstractOpenemsSunSpecComponent
30-
implements SunSpecPvInverter, ManagedSymmetricPvInverter, SymmetricMeter, OpenemsComponent, EventHandler {
32+
public abstract class AbstractSunSpecPvInverter extends AbstractOpenemsSunSpecComponent implements SunSpecPvInverter,
33+
ManagedSymmetricPvInverter, AsymmetricMeter, SymmetricMeter, OpenemsComponent, EventHandler {
3134

3235
private final Logger log = LoggerFactory.getLogger(AbstractSunSpecPvInverter.class);
3336
private final SetPvLimitHandler setPvLimitHandler = new SetPvLimitHandler(this);
3437

38+
private boolean isSinglePhase;
39+
private Phase phase;
40+
3541
public AbstractSunSpecPvInverter(Map<SunSpecModel, Priority> activeModels,
3642
io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds,
3743
io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) throws OpenemsException {
@@ -42,16 +48,42 @@ public AbstractSunSpecPvInverter(Map<SunSpecModel, Priority> activeModels,
4248
/**
4349
* Make sure to call this method from the inheriting OSGi Component.
4450
*
51+
* @param context ComponentContext of this component. Receive it
52+
* from parameter for @Activate
53+
* @param id ID of this component. Typically 'config.id()'
54+
* @param alias Human-readable name of this Component. Typically
55+
* 'config.alias()'. Defaults to 'id' if empty
56+
* @param enabled Whether the component should be enabled.
57+
* Typically 'config.enabled()'
58+
* @param unitId Unit-ID of the Modbus target
59+
* @param cm An instance of ConfigurationAdmin. Receive it
60+
* using @Reference
61+
* @param modbusReference The name of the @Reference setter method for the
62+
* Modbus bridge - e.g. 'Modbus' if you have a
63+
* setModbus()-method
64+
* @param modbusId The ID of the Modbus bridge. Typically
65+
* 'config.modbus_id()'
66+
* @param readFromCommonBlockNo the starting block number
67+
* @param phase the phase the inverter is connected
68+
* @return true if the target filter was updated. You may use it to abort the
69+
* activate() method.
4570
* @throws OpenemsException on error
4671
*/
47-
@Override
4872
protected boolean activate(ComponentContext context, String id, String alias, boolean enabled, int unitId,
49-
ConfigurationAdmin cm, String modbusReference, String modbusId, int readFromCommonBlockNo)
73+
ConfigurationAdmin cm, String modbusReference, String modbusId, int readFromCommonBlockNo, Phase phase)
5074
throws OpenemsException {
75+
this.phase = phase;
5176
return super.activate(context, id, alias, enabled, unitId, cm, modbusReference, modbusId,
5277
readFromCommonBlockNo);
5378
}
5479

80+
@Override
81+
protected boolean activate(ComponentContext context, String id, String alias, boolean enabled, int unitId,
82+
ConfigurationAdmin cm, String modbusReference, String modbusId, int readFromCommonBlockNo)
83+
throws OpenemsException {
84+
throw new IllegalArgumentException("Use the other activate() method.");
85+
}
86+
5587
/**
5688
* Make sure to call this method from the inheriting OSGi Component.
5789
*/
@@ -108,48 +140,122 @@ public String debugLog() {
108140
protected void onSunSpecInitializationCompleted() {
109141
this.logInfo(this.log, "SunSpec initialization finished. " + this.channels().size() + " Channels available.");
110142

111-
/*
112-
* SymmetricMeter
113-
*/
143+
this.channel(SunSpecPvInverter.ChannelId.WRONG_PHASE_CONFIGURED)
144+
.setNextValue(this.isSinglePhase() ? this.phase == Phase.ALL : this.phase != Phase.ALL);
145+
114146
this.mapFirstPointToChannel(//
115147
SymmetricMeter.ChannelId.FREQUENCY, //
116148
ElementToChannelConverter.SCALE_FACTOR_3, //
117149
DefaultSunSpecModel.S111.HZ, DefaultSunSpecModel.S112.HZ, DefaultSunSpecModel.S113.HZ,
118150
DefaultSunSpecModel.S101.HZ, DefaultSunSpecModel.S102.HZ, DefaultSunSpecModel.S103.HZ);
151+
119152
this.mapFirstPointToChannel(//
120153
SymmetricMeter.ChannelId.ACTIVE_POWER, //
121154
ElementToChannelConverter.DIRECT_1_TO_1, //
122155
DefaultSunSpecModel.S111.W, DefaultSunSpecModel.S112.W, DefaultSunSpecModel.S113.W,
123156
DefaultSunSpecModel.S101.W, DefaultSunSpecModel.S102.W, DefaultSunSpecModel.S103.W);
157+
124158
this.mapFirstPointToChannel(//
125159
SymmetricMeter.ChannelId.REACTIVE_POWER, //
126160
ElementToChannelConverter.DIRECT_1_TO_1, //
127161
DefaultSunSpecModel.S111.V_AR, DefaultSunSpecModel.S112.V_AR, DefaultSunSpecModel.S113.V_AR,
128162
DefaultSunSpecModel.S101.V_AR, DefaultSunSpecModel.S102.V_AR, DefaultSunSpecModel.S103.V_AR);
163+
129164
this.mapFirstPointToChannel(//
130165
SymmetricMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, //
131166
ElementToChannelConverter.DIRECT_1_TO_1, //
132167
DefaultSunSpecModel.S111.WH, DefaultSunSpecModel.S112.WH, DefaultSunSpecModel.S113.WH,
133168
DefaultSunSpecModel.S101.WH, DefaultSunSpecModel.S102.WH, DefaultSunSpecModel.S103.WH);
169+
134170
this.mapFirstPointToChannel(//
135-
SymmetricMeter.ChannelId.VOLTAGE, //
136-
ElementToChannelConverter.SCALE_FACTOR_3, //
137-
DefaultSunSpecModel.S111.PH_VPH_A, DefaultSunSpecModel.S111.PH_VPH_B, DefaultSunSpecModel.S111.PH_VPH_C,
138-
DefaultSunSpecModel.S112.PH_VPH_A, DefaultSunSpecModel.S112.PH_VPH_B, DefaultSunSpecModel.S112.PH_VPH_C,
139-
DefaultSunSpecModel.S113.PH_VPH_A, DefaultSunSpecModel.S113.PH_VPH_B, DefaultSunSpecModel.S113.PH_VPH_C,
140-
DefaultSunSpecModel.S101.PH_VPH_A, DefaultSunSpecModel.S101.PH_VPH_B, DefaultSunSpecModel.S101.PH_VPH_C,
141-
DefaultSunSpecModel.S102.PH_VPH_A, DefaultSunSpecModel.S102.PH_VPH_B, DefaultSunSpecModel.S102.PH_VPH_C,
142-
DefaultSunSpecModel.S103.PH_VPH_A, DefaultSunSpecModel.S103.PH_VPH_B,
143-
DefaultSunSpecModel.S103.PH_VPH_C);
171+
ManagedSymmetricPvInverter.ChannelId.MAX_APPARENT_POWER, //
172+
ElementToChannelConverter.DIRECT_1_TO_1, //
173+
DefaultSunSpecModel.S120.W_RTG);
174+
144175
this.mapFirstPointToChannel(//
145176
SymmetricMeter.ChannelId.CURRENT, //
146177
ElementToChannelConverter.SCALE_FACTOR_3, //
147178
DefaultSunSpecModel.S111.A, DefaultSunSpecModel.S112.A, DefaultSunSpecModel.S113.A,
148179
DefaultSunSpecModel.S101.A, DefaultSunSpecModel.S102.A, DefaultSunSpecModel.S103.A);
180+
181+
/*
182+
* SymmetricMeter
183+
*/
184+
if (!this.isSinglePhase) {
185+
this.mapFirstPointToChannel(//
186+
SymmetricMeter.ChannelId.VOLTAGE, //
187+
ElementToChannelConverter.SCALE_FACTOR_3, //
188+
DefaultSunSpecModel.S112.PH_VPH_A, DefaultSunSpecModel.S112.PH_VPH_B,
189+
DefaultSunSpecModel.S112.PH_VPH_C, //
190+
DefaultSunSpecModel.S113.PH_VPH_A, DefaultSunSpecModel.S113.PH_VPH_B,
191+
DefaultSunSpecModel.S113.PH_VPH_C, //
192+
DefaultSunSpecModel.S102.PH_VPH_A, DefaultSunSpecModel.S102.PH_VPH_B,
193+
DefaultSunSpecModel.S102.PH_VPH_C, //
194+
DefaultSunSpecModel.S103.PH_VPH_A, DefaultSunSpecModel.S103.PH_VPH_B,
195+
DefaultSunSpecModel.S103.PH_VPH_C);
196+
return;
197+
}
198+
199+
/*
200+
* AsymmetricMeter
201+
*/
202+
switch (this.phase) {
203+
case ALL:
204+
// use l1 when 'ALL' is configured and its not a tree phase inverter
205+
case L1:
206+
this.mapFirstPointToChannel(AsymmetricMeter.ChannelId.VOLTAGE_L1, //
207+
ElementToChannelConverter.DIRECT_1_TO_1, //
208+
DefaultSunSpecModel.S101.PH_VPH_A, DefaultSunSpecModel.S111.PH_VPH_A);
209+
break;
210+
case L2:
211+
this.mapFirstPointToChannel(AsymmetricMeter.ChannelId.VOLTAGE_L2, //
212+
ElementToChannelConverter.DIRECT_1_TO_1, //
213+
DefaultSunSpecModel.S101.PH_VPH_B, DefaultSunSpecModel.S111.PH_VPH_B);
214+
break;
215+
case L3:
216+
this.mapFirstPointToChannel(AsymmetricMeter.ChannelId.VOLTAGE_L3, //
217+
ElementToChannelConverter.DIRECT_1_TO_1, //
218+
DefaultSunSpecModel.S101.PH_VPH_C, DefaultSunSpecModel.S111.PH_VPH_C);
219+
break;
220+
}
221+
149222
this.mapFirstPointToChannel(//
150-
ManagedSymmetricPvInverter.ChannelId.MAX_APPARENT_POWER, //
223+
SymmetricMeter.ChannelId.VOLTAGE, //
151224
ElementToChannelConverter.DIRECT_1_TO_1, //
152-
DefaultSunSpecModel.S120.W_RTG);
225+
DefaultSunSpecModel.S101.PH_VPH_A, DefaultSunSpecModel.S111.PH_VPH_A, //
226+
DefaultSunSpecModel.S101.PH_VPH_B, DefaultSunSpecModel.S111.PH_VPH_B, //
227+
DefaultSunSpecModel.S101.PH_VPH_C, DefaultSunSpecModel.S111.PH_VPH_C);
228+
229+
}
230+
231+
@Override
232+
protected void addBlock(int startAddress, SunSpecModel model, Priority priority) throws OpenemsException {
233+
super.addBlock(startAddress, model, priority);
234+
235+
if (Lists.newArrayList(DefaultSunSpecModel.S_101, //
236+
DefaultSunSpecModel.S_111) //
237+
.stream() //
238+
.anyMatch(t -> t.equals(model))) {
239+
// single phase
240+
this.isSinglePhase = true;
241+
} else if (Lists.newArrayList(DefaultSunSpecModel.S_102, //
242+
DefaultSunSpecModel.S_112) //
243+
.stream() //
244+
.anyMatch(t -> t.equals(model))) {
245+
// split Phase
246+
this.isSinglePhase = false;
247+
} else if (Lists.newArrayList(DefaultSunSpecModel.S_103, //
248+
DefaultSunSpecModel.S_113) //
249+
.stream() //
250+
.anyMatch(t -> t.equals(model))) {
251+
// three Phase
252+
this.isSinglePhase = false;
253+
}
254+
255+
}
256+
257+
protected final boolean isSinglePhase() {
258+
return this.isSinglePhase;
153259
}
154260

155261
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.openems.edge.pvinverter.sunspec;
2+
3+
public enum Phase {
4+
ALL, //
5+
L1, //
6+
L2, //
7+
L3;
8+
}

io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/SunSpecPvInverter.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ public interface SunSpecPvInverter {
77

88
public static enum ChannelId implements io.openems.edge.common.channel.ChannelId {
99
PV_LIMIT_FAILED(Doc.of(Level.FAULT) //
10-
.text("PV-Limit failed"));
10+
.text("PV-Limit failed")), //
11+
WRONG_PHASE_CONFIGURED(Doc.of(Level.WARNING) //
12+
.text("Configured Phase does not match the Model")), //
13+
;
1114

1215
private final Doc doc;
1316

io.openems.edge.solaredge/src/io/openems/edge/solaredge/pvinverter/SolarEdge.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.openems.edge.meter.api.SymmetricMeter;
3131
import io.openems.edge.pvinverter.api.ManagedSymmetricPvInverter;
3232
import io.openems.edge.pvinverter.sunspec.AbstractSunSpecPvInverter;
33+
import io.openems.edge.pvinverter.sunspec.Phase;
3334
import io.openems.edge.pvinverter.sunspec.SunSpecPvInverter;
3435

3536
@Designate(ocd = Config.class, factory = true)
@@ -91,7 +92,7 @@ protected void setModbus(BridgeModbus modbus) {
9192
@Activate
9293
void activate(ComponentContext context, Config config) throws OpenemsException {
9394
if (super.activate(context, config.id(), config.alias(), config.enabled(), UNIT_ID, this.cm, "Modbus",
94-
config.modbus_id(), READ_FROM_MODBUS_BLOCK)) {
95+
config.modbus_id(), READ_FROM_MODBUS_BLOCK, Phase.ALL)) {
9596
return;
9697
}
9798
}

0 commit comments

Comments
 (0)