Modbus TCP Communication Issues Between OpenEMS on UniPi and Windows Simulator

We’ve deployed OpenEMS Edge and UI on a UniPi device (Base OS, Debian Trixie-based) and are running a Modbus TCP simulator on Windows 11 that emulates real energy assets across multiple ports.

The setup works correctly when using a Raspberry Pi (OpenEMS) and a Mac (simulator), but on UniPi we observe:

  • Data not updating or showing incorrect/false values (e.g., Battery SoC)

  • Intermittent or failed connectivity from Docker containers to the simulator’s IP

  • Empty live/history views in OpenEMS UI

Felix logs currently show no errors. Previous issues (Parsing Response failed, OpenemsException, Unable to set value) were mitigated by adjusting data types/formatting in the simulator.

Modbus registers and components are correctly defined in OpenEMS. Connectivity tests confirm network reachability.

Any insights on Docker networking restrictions on UniPi Base OS, timing issues, or known incompatibilities with Modbus TCP setups would be greatly appreciated.

Thanks.


ON THE SIMULATOR (same registers are on OpenEMS)***

This is a smart meter register map:

Config (static)

  • {address: 0x4003, name: modbus_id, type: uint16, static_value: 1}
  • {address: 0x4004, name: baud_rate, type: uint16, static_value: 6}
  • {address: 0x4007, name: software_version, type: float32, static_value: 1.03}
  • {address: 0x4009, name: hardware_version, type: float32, static_value: 1.03}
  • {address: 0x400F, name: combination_code, type: uint16, static_value: 1}
  • {address: 0x4011, name: parity, type: uint16, static_value: 0}
  • {address: 0x4039, name: stop_bits, type: uint16, static_value: 1}

Voltages

  • {address: 0x5002, name: voltage_l1, type: float32, unit: V,
    excel_column: voltage_l1, random_min: 228.0, random_max: 233.0}
  • {address: 0x5004, name: voltage_l2, type: float32, unit: V,
    excel_column: voltage_l2, random_min: 228.0, random_max: 233.0}
  • {address: 0x5006, name: voltage_l3, type: float32, unit: V,
    excel_column: voltage_l3, random_min: 228.0, random_max: 233.0}

Currents

  • {address: 0x500C, name: current_l1, type: float32, unit: A,
    excel_column: current_l1, random_min: 2.0, random_max: 22.0}
  • {address: 0x500E, name: current_l2, type: float32, unit: A,
    excel_column: current_l2, random_min: 2.0, random_max: 22.0}
  • {address: 0x5010, name: current_l3, type: float32, unit: A,
    excel_column: current_l3, random_min: 2.0, random_max: 22.0}
  • address: 0x5008
    name: current_total
    type: float32
    unit: A
    derived: “current_l1 + current_l2 + current_l3”

Active power

  • address: 0x5012
    name: active_power_total
    type: float32
    unit: kW
    derived: “(voltage_l1*current_l1 + voltage_l2*current_l2 + voltage_l3*current_l3) / 1000”
  • address: 0x5014
    name: active_power_l1
    type: float32
    unit: kW
    derived: “voltage_l1 * current_l1 / 1000”
  • address: 0x5016
    name: active_power_l2
    type: float32
    unit: kW
    derived: “voltage_l2 * current_l2 / 1000”
  • address: 0x5018
    name: active_power_l3
    type: float32
    unit: kW
    derived: “voltage_l3 * current_l3 / 1000”

Reactive power

  • address: 0x501A
    name: reactive_power_total
    type: float32
    unit: kvar
    derived: “active_power_total * 0.10”
  • {address: 0x501C, name: reactive_power_l1, type: float32,
    derived: “active_power_l1 * 0.10”}
  • {address: 0x501E, name: reactive_power_l2, type: float32,
    derived: “active_power_l2 * 0.10”}
  • {address: 0x5020, name: reactive_power_l3, type: float32,
    derived: “active_power_l3 * 0.10”}

Apparent power

  • address: 0x5022
    name: apparent_power_total
    type: float32
    unit: kVA
    derived: “math.sqrt(active_power_total**2 + reactive_power_total**2)”
  • {address: 0x5024, name: apparent_power_l1, type: float32,
    derived: “math.sqrt(active_power_l1**2 + reactive_power_l1**2)”}
  • {address: 0x5026, name: apparent_power_l2, type: float32,
    derived: “math.sqrt(active_power_l2**2 + reactive_power_l2**2)”}
  • {address: 0x5028, name: apparent_power_l3, type: float32,
    derived: “math.sqrt(active_power_l3**2 + reactive_power_l3**2)”}

Power factor

  • address: 0x502A
    name: power_factor
    type: float32
    derived: “active_power_total / max(apparent_power_total, 0.001)”
  • {address: 0x502C, name: power_factor_l1, type: float32,
    derived: “active_power_l1 / max(apparent_power_l1, 0.001)”}
  • {address: 0x502E, name: power_factor_l2, type: float32,
    derived: “active_power_l2 / max(apparent_power_l2, 0.001)”}
  • {address: 0x5030, name: power_factor_l3, type: float32,
    derived: “active_power_l3 / max(apparent_power_l3, 0.001)”}

Frequency

  • {address: 0x503E, name: frequency, type: float32, unit: Hz,
    excel_column: frequency, random_min: 49.95, random_max: 50.05}
  • {address: 0x5132, name: freq_l1, type: float32,
    random_min: 49.95, random_max: 50.05}
  • {address: 0x5134, name: freq_l2, type: float32,
    random_min: 49.95, random_max: 50.05}
  • {address: 0x5136, name: freq_l3, type: float32,
    random_min: 49.95, random_max: 50.05}

Energy (cumulative)

Energy (cumulative) - ALL changed to uint32 Wh

  • address: 0x6000
    name: energy_total
    type: uint32
    unit: Wh
    accumulate: active_power_total_w # Must reference W, not kW
    initial_value: 1500000 # 1500.0 kWh → 1,500,000 Wh

  • address: 0x6010
    name: energy_import
    type: uint32
    unit: Wh
    accumulate: active_power_total_w
    initial_value: 1480000

  • address: 0x6020
    name: energy_export
    type: uint32
    unit: Wh
    static_value: 20000 # 20.0 kWh → 20,000 Wh

  • address: 0x600A
    name: energy_l1
    type: uint32
    unit: Wh
    accumulate: active_power_l1_w
    initial_value: 500000

  • address: 0x600C
    name: energy_l2
    type: uint32
    unit: Wh
    accumulate: active_power_l2_w
    initial_value: 500000

  • address: 0x600E
    name: energy_l3
    type: uint32
    unit: Wh
    accumulate: active_power_l3_w
    initial_value: 500000

  • address: 0x6030
    name: reactive_energy_total
    type: uint32
    unit: Wh # Or use varh if OpenEMS expects it
    accumulate: reactive_power_total_w
    initial_value: 150000

Virtual register to convert kW → W for accumulation

  • address: 0xF100
    name: active_power_total_w
    type: float32
    derived: “active_power_total * 1000”

This is a battery’s register map:

# ── OpenEMS standard header (read-only, static) ───────────────────────

  - {address: 0,   name: ems_hash,         type: uint16, static_value: 0x6201}

  - {address: 1,   name: block_length,      type: uint16, static_value: 199}

  - {address: 2,   name: ver_major,         type: uint16, static_value: 2025}

  - {address: 3,   name: ver_minor,         type: uint16, static_value: 1}

  - {address: 4,   name: ver_patch,         type: uint16, static_value: 1}



  \# ── ESS state (222 / 0x00DE) ──────────────────────────────────────────

  - {address: 222, name: system_state, type: uint16, static_value: 0}

    \# 0=Ok, 1=Info, 2=Warning, 3=Fault



  \# ── SymmetricEss block ────────────────────────────────────────────────

  - address: 302     # 0x012E

    name: soc

    type: uint16

    unit: "%"

    \# SoC is updated by the simulator's internal integration loop.

    \# The static_value seeds the initial display; runtime value comes

    \# from self.state\["soc"\] which is written to this address each cycle.

    static_value: 60



  - {address: 303, name: grid_mode, type: uint16, static_value: 1}

    \# 1=On-Grid, 2=Off-Grid



  - address: 304     # 0x0130  ActivePower float32 (neg=charge, pos=discharge)

    name: active_power_w

    type: float32

    unit: W

    \# Driven by the charge_power_w setpoint from OpenEMS.

    \# Falls back to small random fluctuation if no setpoint received.

    derived: "charge_power_w if abs(charge_power_w) > 10 else random_uniform(-2000, 2000)"



  - {address: 306, name: reactive_power_var, type: float32, static_value: 0.0}

  - {address: 308, name: max_apparent_power,  type: float32, static_value: 50000.0}

  - {address: 310, name: capacity_wh,          type: float32, static_value: 116000.0}



  - {address: 312, name: charge_energy_wh, type: uint32, unit: Wh, 

     accumulate: active_power_w_charge, initial_value: 0, scale: 1}



  - {address: 316, name: discharge_energy_wh, type: uint32, unit: Wh,

     accumulate: active_power_w_discharge, initial_value: 0, scale: 1}



  \# ── ManagedSymmetricEss block ─────────────────────────────────────────

  - {address: 400, name: ess_hash2,          type: uint16, static_value: 0xa3ed}

  - {address: 401, name: ess_block_length,   type: uint16, static_value: 100}

  - {address: 402, name: min_power_setpoint, type: float32, static_value: -50000.0}

  - {address: 404, name: max_power_setpoint, type: float32, static_value: 50000.0}



  \# ── Virtual registers (helper values not in OpenEMS map) ─────────────

  - address: 0xF010

    name: active_power_w_charge

    type: float32

    derived: "abs(active_power_w) if active_power_w < 0 else 0"



  - address: 0xF012

    name: active_power_w_discharge

    type: float32

    derived: "active_power_w if active_power_w > 0 else 0"



  - address: 0xF000

    name: state_derived

    type: uint16

    derived: "1 if active_power_w < -10 else (2 if active_power_w > 10 else 0)"

    \# 0=Idle, 1=Charging, 2=Discharging



  - address: 0xF002

    name: power_kw

    type: float32

    unit: kW

    derived: "active_power_w / 1000"



  - address: 0xF004

    name: remaining_energy_kwh

    type: float32

    unit: kWh

    derived: "capacity_kwh \* soc / 100"



  - {address: 0xF006, name: pack_voltage,  type: float32, static_value: 51.2}

  - address: 0xF008

    name: pack_current

    type: float32

    unit: A

    derived: "active_power_w / max(pack_voltage, 1)"

  - {address: 0xF00A, name: temperature, type: float32,

     random_min: 22.0, random_max: 35.0}



writable_registers:

  \# Primary setpoint — OpenEMS writes here to charge/discharge

  - address: 406     # 0x0196

    name: charge_power_w

    type: float32

    unit: W

    min: -50000

    max: 50000

    default: 0

    \# Negative = Charge, Positive = Discharge (OpenEMS convention)



  - {address: 408, name: set_reactive_power_var,    type: float32,

     min: -50000, max: 50000, default: 0}

  - {address: 410, name: set_active_power_less,     type: float32,

     min: 0, max: 50000, default: 50000}

  - {address: 412, name: set_reactive_power_less,   type: float32,

     min: 0, max: 50000, default: 0}

  - {address: 414, name: set_active_power_greater,  type: float32,

     min: -50000, max: -1, default: -50000}

  - {address: 416, name: set_reactive_power_greater, type: float32,

     min: -50000, max: 0, default: 0}

Can you provide a wireshark dump of your modbus tcp connection?

You can create one with the following command on your unipi:

tcpdump -i <lan-interface> -s 65535 -w <target-file> host <ip-address-of-your-windows> and port <modbus-tcp-port>

Thank you for your response, we have solved that issue.