OpenEMS Edge — FC4 Input Register Read Returns Zero for Custom ESS Bundle

Environment

  • OpenEMS Edge version: 2026.3.0

  • Deployment: Docker container on ARM (Raspberry Pi)

  • Bundle type: Custom ESS bundle (ManagedSymmetricEss)

  • Modbus bridge: Bridge.Modbus.Tcp


Summary

We have developed a custom OpenEMS Edge bundle for a third-party ESS inverter that uses Modbus Function Code 4 (FC4 Read Input Registers) for monitoring data. All channels consistently return 0 despite the Modbus device responding correctly to FC4 requests when tested independently.


What Works

  • The Modbus bridge connects successfully (no connection errors)

  • The component shows active in Felix with all references satisfied

  • GridMode correctly shows 1 (On-Grid) — meaning activate() completes

  • Independent Python test from the same host confirms correct FC4 responses:

  • All other assets on the same OpenEMS instance using FC3 work correctly

  • The Modbus bridge, Felix wiring, and OSGi component lifecycle are all healthy


What Fails

  • All channels mapped via FC4ReadInputRegistersTask return 0

  • No errors in OpenEMS logs related to FC4 reads

  • SOC, ACTIVE_POWER, MAX_APPARENT_POWER all show 0

  • The component is not marked as defective in the bridge worker


Bundle Structure

The bundle follows standard OpenEMS patterns:

@Component(name = "Ess.MyDevice", immediate = true,
           configurationPolicy = ConfigurationPolicy.REQUIRE)
public class EssMyDeviceImpl extends AbstractOpenemsModbusComponent
        implements ManagedSymmetricEss, SymmetricEss,
                   ModbusComponent, OpenemsComponent {

    @Reference(policy = ReferencePolicy.STATIC,
               policyOption = ReferencePolicyOption.GREEDY,
               cardinality = ReferenceCardinality.MANDATORY)
    protected void setModbus(BridgeModbus modbus) {
        super.setModbus(modbus);
    }

    @Activate
    protected void activate(ComponentContext context, Config config)
            throws OpenemsException {
        if (super.activate(context, config.id(), config.alias(),
                config.enabled(), config.modbusUnitId(),
                this.cm, "Modbus", config.modbus_id())) {
            return;
        }
        this._setGridMode(GridMode.ON_GRID);
    }

    @Override
    protected ModbusProtocol defineModbusProtocol() {
        return new ModbusProtocol(this,

            new FC4ReadInputRegistersTask(0x0203, Priority.HIGH,
                m(SymmetricEss.ChannelId.SOC,
                        new UnsignedWordElement(0x0203),
                        ElementToChannelConverter.DIRECT_1_TO_1),
                m(ChannelId.BATTERY_SOH,
                        new UnsignedWordElement(0x0204),
                        ElementToChannelConverter.DIRECT_1_TO_1)
            )
        );
    }
}

Things Already Verified and Fixed

  1. "Modbus" reference name — must match setModbus() with capital M. Using lowercase "modbus" causes updateReferenceFilter to always returntrue, causing infinite early return and GridMode staying -1 (Undefined).Fixed — GridMode now shows 1 (On-Grid) correctly.

  2. activate() early return — added if (super.activate(...)) return;pattern matching Fenecon/Voltfang bundles. Fixed.

  3. @Deactivate with @Override — added to match standard pattern.Fixed.

  4. DefectiveComponents backoff — after connection errors, the bridge worker applies exponential backoff up to 5 minutes. Cleared by Docker restart with simulator running first. Not the current issue.

  5. FC4ReadInputRegistersTask exists in the deployed bridge bundle — confirmed by inspecting the bridge jar. The class is present and correctly sends ReadInputRegistersRequest.


Current Symptom

After all fixes above, the component is healthy, GridMode is correct, no errors, but all FC4-mapped channels return 0. FC3-based bundles on the same bridge work perfectly.


Questions for the Community

  1. Is there any known issue with FC4ReadInputRegistersTask in OpenEMS Edge 2026.x where values are read but not propagated to channels?

  2. Does AbstractOpenemsModbusComponent require any additional setup for FC4 tasks compared to FC3 tasks?

  3. Are there any working examples of custom bundles usingFC4ReadInputRegistersTask that we can reference?

  4. Is there a difference in how the Modbus bridge worker handles FC4 vs FC3 read tasks in terms of scheduling or response handling?


Device Register Map (relevant excerpt)

Address Type Accuracy Meaning
0x0117 int16 0.1 kW Total Active Power
0x0118 int16 0.1 kVar Total Reactive Power
0x0119 uint16 0.1 kVA Total Apparent Power
0x0203 uint16 direct % Battery SOC
0x0204 uint16 direct % Battery SOH
0x0500 uint16 direct System State
0x0501 uint16 direct Grid State

All registers are Input Registers (FC04) per the official device register map.


Any guidance or pointers to working FC4 bundle examples would be greatly appreciated.

FC4 0x0203 → [65, 100]  (SOC=65%, SOH=100%)FC4 0x0117 → [345, ...]  (ActivePower=34500W)

Have you made sure that your device is using Modbus TCP and not Modbus RTU over TCP?

Yes, it is using Modbus TCP
This is most of the implementation file for the bundle

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.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
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.channel.Unit;
import io.openems.common.exceptions.OpenemsException;
import io.openems.common.types.OpenemsType;
import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent;
import io.openems.edge.bridge.modbus.api.BridgeModbus;
import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
import io.openems.edge.bridge.modbus.api.ModbusComponent;
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
import io.openems.edge.bridge.modbus.api.element.SignedWordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
import io.openems.edge.bridge.modbus.api.task.FC4ReadInputRegistersTask;
import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.event.EdgeEventConstants;
import io.openems.edge.common.sum.GridMode;
import io.openems.edge.common.taskmanager.Priority;
import io.openems.edge.ess.api.ManagedSymmetricEss;
import io.openems.edge.ess.api.SymmetricEss;
import io.openems.edge.ess.power.api.Power;

@Designate(ocd = Config.class, factory = true)
@Component(
name = “Ess.Scu.Gres”,
immediate = true,
configurationPolicy = ConfigurationPolicy.REQUIRE
)
@EventTopics({
EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE
})
public class EssScuGresImpl extends AbstractOpenemsModbusComponent
implements EssScuGres, ManagedSymmetricEss, SymmetricEss,
ModbusComponent, OpenemsComponent, EventHandler {

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

    BATTERY_SOH(Doc.of(OpenemsType.INTEGER).unit(Unit.PERCENT)
            .text("Battery SOH. FC04 0x0204.")),

    SYSTEM_STATE(Doc.of(OpenemsType.INTEGER).unit(Unit.NONE)
            .text("System State. FC04 0x0500.")),

    GRID_STATE(Doc.of(OpenemsType.INTEGER).unit(Unit.NONE)
            .text("Grid State. FC04 0x0501."));

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

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

@Reference
private ConfigurationAdmin cm;

@Reference(
    policy = ReferencePolicy.STATIC,
    policyOption = ReferencePolicyOption.GREEDY,
    cardinality = ReferenceCardinality.MANDATORY
)
protected void setModbus(BridgeModbus modbus) {
    super.setModbus(modbus);
}

@Reference(
    policy = ReferencePolicy.STATIC,
    policyOption = ReferencePolicyOption.GREEDY,
    cardinality = ReferenceCardinality.MANDATORY
)
private Power power;

public EssScuGresImpl() {
    super(
        OpenemsComponent.ChannelId.values(),
        ManagedSymmetricEss.ChannelId.values(),
        SymmetricEss.ChannelId.values(),
        ModbusComponent.ChannelId.values(),
        EssScuGresImpl.ChannelId.values()
    );
}

@Activate
protected void activate(ComponentContext context, Config config)
        throws OpenemsException {
    if (super.activate(context, config.id(), config.alias(), config.enabled(),
            config.modbusUnitId(), this.cm, "Modbus", config.modbus_id())) {
        return;
    }
    this._setGridMode(GridMode.ON_GRID);
}

@Modified
protected void modified(ComponentContext context, Config config)
        throws OpenemsException {
    if (super.modified(context, config.id(), config.alias(), config.enabled(),
            config.modbusUnitId(), this.cm, "Modbus", config.modbus_id())) {
        return;
    }
    this._setGridMode(GridMode.ON_GRID);
}

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

@Override
public void handleEvent(Event event) {
    if (!this.isEnabled()) {
        return;
    }
    this._setGridMode(GridMode.ON_GRID);
}

@Override
public Power getPower() {
    return this.power;
}

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

@Override
public void applyPower(int activePowerW, int reactivePowerVar)
        throws OpenemsException {
}

@Override
protected ModbusProtocol defineModbusProtocol() {

    return new ModbusProtocol(this,

        new FC4ReadInputRegistersTask(0x0117, Priority.HIGH,
            m(SymmetricEss.ChannelId.ACTIVE_POWER,
                    new SignedWordElement(0x0117),
                    ElementToChannelConverter.SCALE_FACTOR_2),
            m(SymmetricEss.ChannelId.REACTIVE_POWER,
                    new SignedWordElement(0x0118),
                    ElementToChannelConverter.SCALE_FACTOR_2),
            m(SymmetricEss.ChannelId.MAX_APPARENT_POWER,
                    new UnsignedWordElement(0x0119),
                    ElementToChannelConverter.SCALE_FACTOR_2),
            new DummyRegisterElement(0x011A)
        ),

        new FC4ReadInputRegistersTask(0x011B, Priority.LOW,
            m(SymmetricEss.ChannelId.ACTIVE_CHARGE_ENERGY,
                    new UnsignedDoublewordElement(0x011B),
                    ElementToChannelConverter.SCALE_FACTOR_3),
            m(SymmetricEss.ChannelId.ACTIVE_DISCHARGE_ENERGY,
                    new UnsignedDoublewordElement(0x011D),
                    ElementToChannelConverter.SCALE_FACTOR_3)
        ),

        new FC4ReadInputRegistersTask(0x0203, Priority.HIGH,
            m(SymmetricEss.ChannelId.SOC,
                    new UnsignedWordElement(0x0203),
                    ElementToChannelConverter.DIRECT_1_TO_1),
            m(ChannelId.BATTERY_SOH,
                    new UnsignedWordElement(0x0204),
                    ElementToChannelConverter.DIRECT_1_TO_1)
        ),

        new FC4ReadInputRegistersTask(0x0500, Priority.HIGH,
            m(ChannelId.SYSTEM_STATE,
                    new UnsignedWordElement(0x0500),
                    ElementToChannelConverter.DIRECT_1_TO_1),
            m(ChannelId.GRID_STATE,
                    new UnsignedWordElement(0x0501),
                    ElementToChannelConverter.DIRECT_1_TO_1)
        )
    );
}

@Override
public String debugLog() {
    return "SoC:" + this.getSoc().asString()
         + "|P:" + this.getActivePower().asString()
         + "|State:" + this.channel(ChannelId.SYSTEM_STATE).value().asString();
}

}

Looks good to me. Can you create a wireshark dump (with tcpdump) and take a look at the modbus communication? Here, you can find important information:

  1. Is OpenEMS sending a FC4 request at all?

  2. The response to this FC4 frames, especially the value