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}