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: 2 additions & 0 deletions io.openems.edge.ess.srne/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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,\
Expand Down
34 changes: 34 additions & 0 deletions io.openems.edge.ess.srne/readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Provides read-only integration of an SRNE hybrid inverter with OpenEMS Edge.
The bundle reads ESS telemetry from an SRNE inverter over Modbus RTU. It maps
the validated battery, grid-input and backup-output registers to the standard
OpenEMS `SymmetricEss`, `HybridEss` and `ElectricityMeter` channels.
The battery-inverter component also derives the OpenEMS grid/off-grid
lifecycle from machine-state register `0x0210`.

== Supported Devices

Expand Down Expand Up @@ -44,6 +46,38 @@ Control-register writes are not part of this component.

https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.ess.srne[Source Code icon:github[]]

[[_srne_battery_inverter]]

=== SRNE Battery-Inverter

*Name*: SRNE Battery-Inverter

*Factory-PID*: `Srne.BatteryInverter`

.Implemented Natures/Interfaces
* OffGridBatteryInverter
* ManagedSymmetricBatteryInverter (read-only until the control path is implemented)
* ModbusComponent

*Description*: Reads machine state, battery voltage and signed battery current.
Active power is calculated from battery voltage and current. Stable mains and
inverter-powered states map to OpenEMS on-grid and off-grid modes. Startup,
transition, stopped, fault and unknown states intentionally report an undefined
grid mode. State `2` (`running (mains/bypass)`) is validated on the live unit;
the remaining state labels still require live transition testing. No control
register is written by this component.

.*Configuration*:
* `id` (String): Unique component ID (default: `batteryInverter0`)
* `alias` (String): Human-readable component name
* `enabled` (boolean): Enables the component (default: `true`)
* `startStop` (enum): Start/stop behaviour (default: `AUTO`)
* `modbus_id` (String): Modbus bridge component ID (default: `modbus0`)
* `modbusUnitId` (int): Device Unit-ID (default: `1`)
* `maxApparentPower` (int): Maximum inverter apparent power in VA (default: `12000`)

https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.ess.srne[Source Code icon:github[]]

[[_srne_grid_meter]]

=== SRNE Grid-Meter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.openems.edge.ess.srne.batteryinverter;

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

import io.openems.edge.common.startstop.StartStopConfig;
import io.openems.edge.ess.srne.SrneConstants;

@ObjectClassDefinition(//
name = "SRNE Battery-Inverter", //
description = "Implements the off-grid battery-inverter nature of the SRNE ASP48120SH3.")
@interface Config {

@AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
String id() default "batteryInverter0";

@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 = "Start/stop behaviour?", description = "Should this Component be forced to start or stop?")
StartStopConfig startStop() default StartStopConfig.AUTO;

@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 SrneConstants.DEFAULT_UNIT_ID;

@AttributeDefinition(name = "Maximum apparent power", description = "Maximum inverter apparent power in [VA].")
int maxApparentPower() default 12_000;

String webconsole_configurationFactory_nameHint() default "SRNE Battery-Inverter [{id}]";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.openems.edge.ess.srne.batteryinverter;

import io.openems.common.channel.Unit;
import io.openems.common.types.OpenemsType;
import io.openems.edge.batteryinverter.api.OffGridBatteryInverter;
import io.openems.edge.common.channel.Channel;
import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.channel.IntegerReadChannel;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.ess.srne.batteryinverter.statemachine.StateMachine.State;
import io.openems.edge.ess.srne.common.Srne;
import io.openems.edge.ess.srne.common.enums.MachineState;

public interface SrneBatteryInverter extends Srne, OffGridBatteryInverter, OpenemsComponent {

public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
BATTERY_VOLTAGE(Doc.of(OpenemsType.INTEGER) //
.unit(Unit.MILLIVOLT)), //
BATTERY_CURRENT(Doc.of(OpenemsType.INTEGER) //
.unit(Unit.MILLIAMPERE)), //
MACHINE_STATE(Doc.of(MachineState.values()) //
.text("Machine state from holding register 0x0210")), //
STATE_MACHINE(Doc.of(State.values()) //
.text("Grid/off-grid lifecycle derived from the machine state")); //

private final Doc doc;

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

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

/**
* Gets the machine-state channel.
*
* @return the channel
*/
public default Channel<MachineState> getMachineStateChannel() {
return this.channel(ChannelId.MACHINE_STATE);
}

/**
* Gets the current machine state.
*
* @return the current state
*/
public default MachineState getMachineState() {
return this.getMachineStateChannel().value().asEnum();
}

/**
* Gets the battery-voltage channel.
*
* @return the channel
*/
public default IntegerReadChannel getBatteryVoltageChannel() {
return this.channel(ChannelId.BATTERY_VOLTAGE);
}

/**
* Gets the battery-current channel.
*
* @return the channel
*/
public default IntegerReadChannel getBatteryCurrentChannel() {
return this.channel(ChannelId.BATTERY_CURRENT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package io.openems.edge.ess.srne.batteryinverter;

import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_2;
import static org.osgi.service.component.annotations.ReferenceCardinality.MANDATORY;
import static org.osgi.service.component.annotations.ReferencePolicy.STATIC;
import static org.osgi.service.component.annotations.ReferencePolicyOption.GREEDY;

import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

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.metatype.annotations.Designate;

import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.exceptions.OpenemsException;
import io.openems.common.referencetarget.GenerateTargetsFromReferences;
import io.openems.edge.battery.api.Battery;
import io.openems.edge.batteryinverter.api.ManagedSymmetricBatteryInverter;
import io.openems.edge.batteryinverter.api.OffGridBatteryInverter;
import io.openems.edge.batteryinverter.api.SymmetricBatteryInverter;
import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent;
import io.openems.edge.bridge.modbus.api.BridgeModbus;
import io.openems.edge.bridge.modbus.api.ModbusComponent;
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
import io.openems.edge.bridge.modbus.api.element.SignedWordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
import io.openems.edge.common.channel.value.Value;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.startstop.StartStop;
import io.openems.edge.common.startstop.StartStoppable;
import io.openems.edge.common.taskmanager.Priority;
import io.openems.edge.ess.srne.batteryinverter.statemachine.StateMachine;
import io.openems.edge.ess.srne.batteryinverter.statemachine.StateMachine.State;
import io.openems.edge.ess.srne.common.Srne;
import io.openems.edge.ess.srne.common.enums.MachineState;

@Designate(ocd = Config.class, factory = true)
@Component(//
name = "Srne.BatteryInverter", //
immediate = true, //
configurationPolicy = ConfigurationPolicy.REQUIRE //
)
@GenerateTargetsFromReferences("Modbus")
public class SrneBatteryInverterImpl extends AbstractOpenemsModbusComponent
implements SrneBatteryInverter, Srne, OffGridBatteryInverter, ManagedSymmetricBatteryInverter,
SymmetricBatteryInverter, ModbusComponent, OpenemsComponent, StartStoppable {

private final AtomicReference<TargetGridMode> targetGridMode = new AtomicReference<>(TargetGridMode.GO_ON_GRID);
private final AtomicReference<StartStop> startStopTarget = new AtomicReference<>(StartStop.UNDEFINED);

@Override
@Reference(//
policy = STATIC, policyOption = GREEDY, cardinality = MANDATORY, //
target = "(&(id=${config.modbus_id})(enabled=true))")
protected void setModbus(BridgeModbus modbus) {
super.setModbus(modbus);
}

public SrneBatteryInverterImpl() {
super(//
OpenemsComponent.ChannelId.values(), //
ModbusComponent.ChannelId.values(), //
SymmetricBatteryInverter.ChannelId.values(), //
ManagedSymmetricBatteryInverter.ChannelId.values(), //
StartStoppable.ChannelId.values(), //
OffGridBatteryInverter.ChannelId.values(), //
Srne.ChannelId.values(), //
SrneBatteryInverter.ChannelId.values() //
);
}

@Activate
private void activate(ComponentContext context, Config config) throws OpenemsException {
super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId());
this._setMaxApparentPower(config.maxApparentPower());
this.getMachineStateChannel().onSetNextValue(ignore -> this.updateLifecycle());

/*
* SRNE battery current is positive while charging and negative while
* discharging. OpenEMS battery-inverter power uses the opposite sign.
*/
final Consumer<Value<Integer>> updateActivePower = ignore -> {
var voltage = this.getBatteryVoltageChannel().getNextValue().get();
var current = this.getBatteryCurrentChannel().getNextValue().get();
if (voltage == null || current == null) {
this._setActivePower(null);
return;
}
this._setActivePower((int) Math.round(-voltage * (double) current / 1_000_000D));
};
this.getBatteryVoltageChannel().onSetNextValue(updateActivePower);
this.getBatteryCurrentChannel().onSetNextValue(updateActivePower);
}

@Override
@Deactivate
protected void deactivate() {
super.deactivate();
}

private void updateLifecycle() {
MachineState machineState = this.getMachineStateChannel().getNextValue().asEnum();
var state = StateMachine.fromMachineState(machineState);
this.channel(SrneBatteryInverter.ChannelId.STATE_MACHINE).setNextValue(state);
this._setGridMode(StateMachine.toGridMode(state));
switch (state) {
case ON_GRID, OFF_GRID, TRANSITIONING:
this._setInverterState(true);
this._setStartStop(StartStop.START);
break;
case STOPPED, FAULT:
this._setInverterState(false);
this._setStartStop(StartStop.STOP);
break;
case UNDEFINED, STARTING:
this._setInverterState(null);
this._setStartStop(StartStop.UNDEFINED);
break;
}
}

@Override
protected ModbusProtocol defineModbusProtocol() {
return new ModbusProtocol(this, //
new FC3ReadRegistersTask(0x0101, Priority.HIGH, //
m(SrneBatteryInverter.ChannelId.BATTERY_VOLTAGE, new UnsignedWordElement(0x0101),
SCALE_FACTOR_2), //
m(SrneBatteryInverter.ChannelId.BATTERY_CURRENT, new SignedWordElement(0x0102),
SCALE_FACTOR_2)), //
new FC3ReadRegistersTask(0x0210, Priority.HIGH, //
m(SrneBatteryInverter.ChannelId.MACHINE_STATE, new UnsignedWordElement(0x0210))));
}

@Override
public void setTargetGridMode(TargetGridMode targetGridMode) {
this.targetGridMode.set(targetGridMode);
}

@Override
public void setStartStop(StartStop value) throws OpenemsNamedException {
this.startStopTarget.set(value);
}

@Override
public void run(Battery battery, int setActivePower, int setReactivePower) throws OpenemsNamedException {
/*
* Story 18 is intentionally read-only. Story 19 will consume targetGridMode and
* apply the power set-points through the validated FC16 control path.
*/
this.updateLifecycle();
}

@Override
public int getPowerPrecision() {
return 1;
}

@Override
public boolean isManaged() {
return false;
}

@Override
public boolean isOffGridPossible() {
return true;
}

@Override
public String debugLog() {
return "State:" + this.getMachineStateChannel().value().asString() //
+ "|Grid:" + this.getGridModeChannel().value().asString() //
+ "|P:" + this.getActivePower().asString();
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@org.osgi.annotation.versioning.Version("1.0.0")
package io.openems.edge.ess.srne.batteryinverter;
Loading
Loading