Victron Lithium NG batteries and the Lynx Smart BMS NG: system design, parallel BMSes, and reading the data

How Victron's Lithium NG batteries and Lynx Smart BMS NG fit together, how to wire a single bank and up to five BMSes in parallel, and how to read every value from code over Modbus TCP, MQTT, and D-Bus.

TTMH20 min read8 sections
Batteries
On this page
  1. Part 1 — Lithium NG batteries in brief
  2. Part 2 — The Lynx Smart BMS NG
  3. Part 3 — The general schematic
  4. Part 4 — Connecting Lynx BMSes in parallel
  5. Part 5 — Reading the data as a programmer
  6. The mistakes that cost money
  7. Sources
  8. Related reading

Victron Lithium NG batteries and the Lynx Smart BMS NG

Victron's Lithium NG ("next generation") batteries and the Lynx Smart BMS NG are designed as one system: the batteries measure, the BMS decides. This guide covers the three things people ask about once they get past the brochure — what the parts actually do, how to wire one bank or several banks in parallel, and how to get the numbers out of the system from your own code.

Every figure below comes from Victron's own manuals, datasheets and open-source code, linked under Sources and checked on 22 October 2026. Firmware moves on; when a number matters for your installation, confirm it against the current manual.

→ Size the bank against your own boat first →


Part 1 — Lithium NG batteries in brief

Lithium NG batteries are LiFePO₄ modules in 12.8 V, 25.6 V and 51.2 V versions. Each battery has integrated cell balancing and cell monitoring and reports cell voltages, temperatures and battery current to the BMS over its BMS cables. What it does not do is make decisions: stopping a charge, disconnecting loads and publishing charge limits are the BMS's job, and Victron lists the BMS as mandatory. The BMS also updates the battery firmware automatically.

ModelNominalCapacityEnergyMax continuous discharge10 s pulseWeight (est.)
LFP-12,8/10012.8 V100 Ah1,280 Wh100 A (1C)200 A9 kg
LFP-12,8/15012.8 V150 Ah1,920 Wh150 A (1C)300 A14 kg
LFP-12,8/20012.8 V200 Ah2,560 Wh200 A (1C)400 A19 kg
LFP-12,8/30012.8 V300 Ah3,840 Wh300 A (1C)600 A29 kg
LFP-25,6/10025.6 V100 Ah2,560 Wh100 A (1C)200 A19 kg
LFP-25,6/20025.6 V200 Ah5,120 Wh200 A (1C)400 A37 kg
LFP-25,6/30025.6 V300 Ah7,680 Wh300 A (1C)600 A52 kg
LFP-51,2/10051.2 V100 Ah5,120 Wh100 A (1C)200 A37 kg

Source: Victron Lithium NG datasheet. Energy and capacity at 25 °C and ≤1C discharge.

What the BMS enforces

The datasheet lists three cell-level thresholds the BMS acts on:

  • Pre-alarm when any cell falls below 3.0 V
  • Load disconnect when any cell falls below 2.8 V
  • Charge stop when any cell rises above 3.6 V, or the temperature is too high or too low

Charging is allowed between +5 °C and +50 °C; discharging between −20 °C and +50 °C.

Series and parallel limits

RuleLimit
In seriesMax four 12.8 V or two 25.6 V batteries — so 51.2 V nominal is the ceiling
In parallelUp to 50 batteries (12.8 V or 25.6 V), 25 with 51.2 V batteries
Per BMSUp to 192 kWh (12 V), 384 kWh (24 V), 128 kWh (48 V)
Series stringsSame capacity and part number in a string; ages within 3 years; each battery fully charged and balanced before connecting
Mid-pointsNever interconnect them or connect anything to them

The 48 V ceiling matters for propulsion. Lithium NG systems stop at the 48 V class. If your configurator result points to a 96 V bus — as it does from about 15 kW upwards — Lithium NG is the wrong family, and you are into a dedicated propulsion pack with its own BMS. See the BMS guide and 48 V vs 96 V.

Which BMS

Three BMSes work with Lithium NG: the Lynx Smart BMS NG (500 A or 1000 A), the smallBMS NG and the VE.Bus BMS NG. For propulsion-scale currents the Lynx is the relevant one, and the rest of this guide is about it. Do not confuse it with the older Lynx Smart BMS (no "NG"), which is for Victron's Lithium Smart batteries.


Part 2 — The Lynx Smart BMS NG

The Lynx Smart BMS NG is a module on Victron's M10 Lynx busbar that combines:

  • A contactor — 500 A or 1000 A continuous, 600 A or 1200 A for 5 minutes. It is the backstop: the primary controls are the ATC/ATD contacts and DVCC, and the contactor opens if those fail to stop a charger or load. It doubles as a remote main switch.
  • Pre-charge for capacitive loads such as inverters, so closing the contactor does not draw a large inrush current.
  • A battery monitor (shunt and electronics) for state of charge, time-to-go and history.
  • ATC and ATD contacts — potential-free contacts that enable chargers (allowed-to-charge) and loads (allowed-to-discharge).
  • A pre-alarm with at least 30 seconds' warning before a low-cell shutdown, via the programmable relay — which can alternatively shut down an alternator.
  • An AUX output at battery voltage, rated 1.1 A continuous, meant to keep the GX device powered after a low-cell shutdown — it stays on for up to 5 minutes.
  • Bluetooth for VictronConnect, VE.Can for a GX device or an NMEA 2000 network, and RJ10 links to Lynx Distributors for fuse monitoring.

Through a GX device the BMS runs DVCC: it publishes a charge voltage limit (CVL), charge current limit (CCL) and discharge current limit (DCL), and compatible Victron inverter/chargers, MPPTs and Orion XS chargers follow them with no extra wiring.

It does not fuse anything. Fusing is external — Lynx Distributors or separate fuse holders — and the total of the fuse values must not exceed the rating of the lowest-rated Lynx module in the chain.

Sizing the BMS against your current

ModelContinuous5-minute peakAt 12 VAt 24 VAt 48 V
Lynx Smart BMS NG 500 A500 A600 A6 kW12 kW24 kW
Lynx Smart BMS NG 1000 A1000 A1200 A12 kW24 kW48 kW

Compare these with the DC design current from the configurator, which is sized at minimum battery voltage with margin. Its 30 ft sailboat example — 8 kW at 48 V — comes out at about 280 A, comfortably inside a 500 A unit. Push the same 48 V system to 15 kW and the design current is about 520 A: past the 500 A model, so either the 1000 A model or two 500 A units in parallel (Part 4) — or, at that power, a 96 V system instead.


Part 3 — The general schematic

LITHIUM NGbattery 1FUSELITHIUM NGbattery 2LITHIUM NGbattery 3LYNX DISTRIBUTORbattery sideLYNX SMART BMS NGcontactor · shunt · pre-chargeLYNX DISTRIBUTORfused outputsBATTERIES LEFT · LOADS RIGHTINVERTER/CHARGERMultiPlus / QuattroMOTOR CONTROLLERpropulsionDC LOADSand DC chargersM8 BMS CABLES — FIRST BATTERYLAST BATTERY — BACK TO THE BMSATC / ATD — CHARGE & LOAD ENABLECERBO GXDVCC · Modbus · MQTT · VRMVE.CANAUXTERMINATE VE.CAN AT BOTH ENDSDC NEGATIVE
DC positiveDC negativeData: BMS cables, VE.Can, ATC/ATD
General system: Lithium NG batteries (each fused on the positive) feed the battery side of the Lynx busbar; the Lynx Smart BMS NG sits in the middle with its contactor, shunt and pre-charge; loads and chargers take fused outputs on the right. The short dotted links between the Lynx modules are the RJ10 fuse-status cables. The batteries report cell data over the daisy-chained M8 BMS cables — first and last battery both plug into the BMS. The GX device talks to the BMS over VE.Can and is powered from the BMS AUX output so it stays up after a low-cell shutdown.

The layout follows Victron's own system example: batteries on the left of the BMS, loads and chargers on the right (reversed if the BMS is mounted upside-down). Wiring checklist:

  1. Fuse every battery on its positive. In series/parallel banks, fuse each series string, and fuse the main positive cable to the bank.
  2. Parallel batteries take diagonal connections — system positive at one end, negative at the other — so every battery sees the same current path. The system cable's cross-section should equal the string cable's times the number of strings.
  3. Daisy-chain the BMS cables. Each battery has a 50 cm M8 male and an M8 female BMS cable. Interconnect the batteries, then plug the free cable of the first and the last battery into the two BMS connectors. Extensions are sold in lengths from 1 to 5 m.
  4. M10 lugs on everything that lands on the BMS, torqued to 33 Nm.
  5. RJ10 from the BMS to each Lynx Distributor so blown fuses raise an alarm (Distributors from serial HQ1909 onwards).
  6. VE.Can (RJ45) from the BMS to the GX device, with a VE.Can terminator at the first and the last device of the chain.
  7. Power the GX from the BMS AUX output, so the system can still report what happened after a low-cell shutdown.
  8. ATC to the chargers' remote on/off, ATD to the loads' remote on/off. DVCC handles compatible Victron gear over the network; the contacts cover everything that doesn't speak DVCC.

Part 4 — Connecting Lynx BMSes in parallel

Since BMS firmware v1.11 and Venus OS 3.40, several Lynx BMSes — each with its own battery bank — can be combined into one parallel redundant battery system. It raises the total capacity and current, and it adds redundancy: if one bank faults, its BMS disconnects it and the rest keep running.

BANK 1Lithium NG batteriesFUSEDLYNX BMS 1own contactor & shuntBANK 2Lithium NG batteriesLYNX BMS 2own contactor & shuntEQUALLENGTHLYNX DISTRIBUTORcommon busbarLOADS & CHARGERSsee the combined limitsCABLE ACABLE BCERBO GXVenus OS ≥ 3.40AUX + DIODES → GX POWERGX: ONE VIRTUAL BATTERYbattery.lynxparallel<N>CCL/DCL SUMMED · CVL LOWEST
DC positive / AUXVE.Can
Two banks, one system. Each Lynx BMS manages its own bank; the GX groups BMSes that share a VE.Can instance into one virtual battery that DVCC uses. Keep the load-side cables to the common busbar equal in length so the banks share current, parallel the AUX outputs through blocking diodes so the GX survives one BMS switching off, and wire the ATC and ATD contacts of all BMSes in parallel (not drawn). Negatives omitted for clarity.

Requirements and limits

RequirementDetail
Number of BMSesUp to 5
BMS firmwarev1.11 or later on every BMS
GX firmwareVenus OS 3.40 or later
Current ratingAll the same — 500 A with 500 A, or 1000 A with 1000 A; never mixed
Mixing familiesLynx Smart BMS and Lynx BMS NG can be combined — so a Lithium Smart bank and a Lithium NG bank can share one system

How the grouping works

The GX device treats every BMS that shares a VE.Can instance as part of one bank on the same DC bus, and creates a virtual battery that appears as an extra device in the device list. DVCC selects it automatically, and new banks join without configuration. If you have several Lynx BMSes on one VE.Can network that are not meant to form a bank, give each a unique VE.Can instance on the GX (Settings → Services → the corresponding VE.Can port).

Wiring particulars

  • Equal load-side cables. The cables from each BMS to the common Lynx Distributor or busbar must be the same length so the banks share current evenly.
  • AUX outputs in parallel, through a blocking diode each, so the GX stays powered when one BMS is switched off.
  • ATC contacts in parallel. The BMSes follow each other's ATC state — if one disables charging, they all do. If an alternator relay was designed in, parallel those contacts too; on new systems the alternator can go straight on the ATC line, because the BMS waits 2 seconds after ATC before opening its contactor.
  • ATD contacts in parallel, so loads stay on while at least one BMS is live.
  • Recommended: a latching push button with LED on each BMS's remote on/off terminals (a ballast resistor is needed for 12 V/24 V buttons on a 48 V bank), and a buzzer on GX relay 1 set as the alarm relay.

Joining and leaving

When a BMS powers up it compares its bank voltage with the system voltage and stays pending until the difference is small enough to close its contactor safely — the more capacity already online, the smaller the permitted difference. An empty bank added to a full system therefore waits until the system discharges to a similar voltage; a full bank added to an empty system waits until it has been charged up.

What the virtual battery reports

ValueHow the GX combines it
Charge voltage limit (CVL)Lowest of the BMSes
Charge current limit (CCL)Sum
Discharge current limit (DCL)Sum
State of chargeAverage, weighted by each bank's share of capacity
Capacity (Ah)Sum
Time to goAverage
VoltageAverage
Current and powerSum
TemperatureMaximum

The current limits are always the sum of the active BMSes. When one disconnects, the system's current capability drops with it.

Design for N−1 if you are counting on the redundancy. This is our recommendation rather than Victron's wording: two 500 A units give 1000 A together but only 500 A after one drops out, so check that the loads you must keep running fit within the remaining BMSes.

When something breaks

Victron's manual describes these failure cases for two BMSes chained BMS 1 → BMS 2 → GX (cables A and B in the drawing):

EventWhat happens
Cable A (BMS 1 to BMS 2) brokenBoth BMSes run standalone; BMS 2 keeps DVCC with the GX; the virtual battery stays and shows one of two BMSes connected
Cable B (BMS 2 to GX) brokenBoth BMSes keep running in parallel, but DVCC stops — chargers fall back to their own charge algorithm — and the virtual battery disappears
Error in one bankThat BMS disconnects its bank; DVCC limits come from the remaining BMS
Power failure in one BMSThat BMS turns off; the others carry on

Part 5 — Reading the data as a programmer

LITHIUM NGcell V, temp, currentM8LYNX SMART BMS NGlimits · SoC · alarmsVICTRONCONNECTBluetooth · Instant ReadoutNMEA 2000 / MFDPGN 127506 · 127508 · 127501VE.CAN → MICRO-CVE.CANGX DEVICED-Bus: com.victronenergy.battery.*MODBUS TCPport 502 · battery registersMQTT1883 / 8883 · N/<portal>/battery/…VRM PORTALcloud · VictronConnect Remote
Data path
Where a program can read the data. The batteries talk only to the BMS. For software, the GX device is the practical hub: it puts every BMS — and the virtual parallel battery — on its internal D-Bus and serves the same values over Modbus TCP and MQTT on the local network.

Choosing an interface

InterfaceWhat you getUse it for
Bluetooth (VictronConnect)Full setup, monitoring, history; Instant Readout shows voltage, current, SoC and time remaining in the device listPeople, not programs
NMEA 2000 (VE.Can to micro-C cable)PGNs 126996 product info, 127506 DC detailed status, 127508 battery status, 127501 switch bank status (contactor, alarm, low/high voltage, relay)Chart plotters and MFDs
GX device: Modbus TCPFixed register map, polledPLCs, SCADA, simple scripts
GX device: MQTTEvery D-Bus value, pushed on changeDashboards, Node-RED, Home Assistant, anything event-driven
GX device: D-BusThe source all of the above read fromCode running on the GX itself

For software, the GX device is the practical hub: it is the one documented, supported way to get every BMS value — including the virtual parallel battery — onto your network.

Turning access on

  • Modbus TCP: on current firmware, Settings → Integrations → Modbus TCP server (older firmware: Settings → Services → Modbus TCP). It listens on port 502; function codes 3 and 4 both work.
  • MQTT: Settings → Integrations → MQTT Access — a security profile must be set first under Settings → General (older firmware: Settings → Services → MQTT). Ports 1883, or 8883 with TLS. Topics include your VRM Portal ID, found under Settings → VRM online portal.

The values for a single BMS

Each Lynx BMS appears on the GX as a com.victronenergy.battery service. These are the D-Bus paths and Modbus registers that matter most, from Victron's register list. The rule for every register: real value = raw value ÷ scale factor, sign included.

D-Bus pathRegisterTypeScaleMeaning
/Dc/0/Voltage259uint16100Battery voltage (V)
/Dc/0/Current261int1610Battery current (A), positive = charging
/Dc/0/Power256int321Power (W) — see note below
/Dc/0/Temperature262int1610Battery temperature (°C)
/Soc266uint1610State of charge (%)
/ConsumedAmphours265uint16−10Consumed Ah (negative when discharged)
/TimeToGo303uint160.01Time to go (s)
/Capacity309uint1610Capacity (Ah)
/Info/MaxChargeVoltage305uint1610CVL (V)
/Info/MaxChargeCurrent307uint1610CCL (A)
/Info/MaxDischargeCurrent308uint1610DCL (A)
/State1282uint1619 = Running, 14 = Standby, 10 = Error …
/ErrorCode1283uint1610 = no error
/System/NrOfBatteries1286uint161Batteries on the BMS
/System/BatteriesParallel / Series1287 / 1288uint161Bank layout the BMS detected
/System/MinCellVoltage / Max1290 / 1291uint16100Lowest / highest cell (V)
/System/MinCellTemperature / Max318 / 319int1610Coldest / hottest cell (°C)
/Io/AllowToCharge / Discharge1297 / 1298uint161ATC / ATD state
/Alarms/LowCellVoltage326uint1610 = OK, 1 = almost discharged, 2 = alarm

Three details that trip people up:

  • Read power from register 256, not 258. Register 258 is the legacy int16 version and tops out at 32,767 W. A 1000 A Lynx at 48 V can move 48 kW. Register 256 is an int32, sent high word first.
  • The unit ID is not the device instance. Modbus unit IDs are one byte, 0–255; the GX translates. A CAN-bus BMS on device instance 512 is unit ID 225, and unit 100 is the system overview. Always take the unit ID from Modbus TCP server → Available services on the GX — and if a device is not listed there, it has no Modbus address and MQTT is the way in.
  • Signed registers are two's complement. Current and temperature go negative.

Example: polling one BMS over Modbus TCP

Standard-library Python, no Modbus package needed. We tested the decoding against a simulated GX built from Victron's own register definitions.

import socket
import struct


def _recv_exact(sock, n):
    buf = b""
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise ConnectionError("GX closed the connection")
        buf += chunk
    return buf


def read_registers(host, unit_id, start, count, port=502, timeout=3.0):
    """Function code 3 (read holding registers). Returns a list of uint16."""
    if not 0 <= unit_id <= 255:
        raise ValueError("Modbus unit IDs are 0-255 - use the unit ID the GX lists, "
                         "not the device instance (e.g. instance 512 is unit 225)")
    # MBAP header: transaction id, protocol id (0), length, unit id; then the PDU.
    request = struct.pack(">HHHBBHH", 1, 0, 6, unit_id, 3, start, count)
    with socket.create_connection((host, port), timeout=timeout) as sock:
        sock.sendall(request)
        _, _, length, _ = struct.unpack(">HHHB", _recv_exact(sock, 7))
        pdu = _recv_exact(sock, length - 1)
    if pdu[0] & 0x80:
        raise IOError(f"Modbus exception {pdu[1]} reading {start} on unit {unit_id}")
    return list(struct.unpack(f">{count}H", pdu[2:2 + 2 * count]))


def s16(raw):
    """uint16 register -> signed int16."""
    return raw - 0x10000 if raw & 0x8000 else raw


def s32(high, low):
    """Two registers -> signed int32. Victron sends the high word first."""
    raw = (high << 16) | low
    return raw - 0x1_0000_0000 if raw & 0x8000_0000 else raw


def read_lynx_bms(host, unit_id, port=502):
    r = lambda start, count=1: read_registers(host, unit_id, start, count, port)
    power_hi, power_lo = r(256, 2)              # /Dc/0/Power, int32 W
    cvl = r(305)[0] / 10                        # /Info/MaxChargeVoltage
    ccl, dcl = (x / 10 for x in r(307, 2))      # /Info/MaxChargeCurrent, /Info/MaxDischargeCurrent
    cell_min, cell_max = (x / 100 for x in r(1290, 2))
    allow_charge, allow_discharge = r(1297, 2)  # /Io/AllowToCharge, /Io/AllowToDischarge
    return {
        "voltage_V": r(259)[0] / 100,           # /Dc/0/Voltage, uint16, scale 100
        "current_A": s16(r(261)[0]) / 10,       # /Dc/0/Current, int16, + = charging
        "power_W": s32(power_hi, power_lo),     # use 256 (int32), not 258 (int16)
        "temperature_C": s16(r(262)[0]) / 10,   # /Dc/0/Temperature
        "soc_pct": r(266)[0] / 10,              # /Soc
        "consumed_Ah": r(265)[0] / -10,         # /ConsumedAmphours, scale -10
        "capacity_Ah": r(309)[0] / 10,          # /Capacity
        "cvl_V": cvl, "ccl_A": ccl, "dcl_A": dcl,
        "cell_min_V": cell_min, "cell_max_V": cell_max,
        "batteries": r(1286)[0],                # /System/NrOfBatteries
        "in_parallel": r(1287)[0],              # /System/BatteriesParallel
        "in_series": r(1288)[0],                # /System/BatteriesSeries
        "allow_to_charge": bool(allow_charge),
        "allow_to_discharge": bool(allow_discharge),
        "state": r(1282)[0],                    # /State, 9 = Running
        "error": r(1283)[0],                    # /ErrorCode, 0 = no error
    }


if __name__ == "__main__":
    # Take the unit ID from the GX: Settings → Integrations → Modbus TCP server
    # → Available services. A CAN-bus BMS on device instance 512 is unit 225.
    print(read_lynx_bms("192.168.1.50", unit_id=225))

On the GX itself: D-Bus

If your code runs on the GX (a Venus OS add-on, for example), read D-Bus directly. From a root shell, dbus -y lists the services, dbus-spy browses them interactively, and single values read like this:

# the battery the system is using, via the system overview service
dbus -y com.victronenergy.system /Dc/Battery/Soc GetValue

# every value of one battery service (use the name `dbus -y` lists)
dbus -y com.victronenergy.battery.<service-name> / GetValue

Several Lynx BMSes in parallel

With paralleled BMSes the GX publishes one battery service per physical BMS plus one virtual battery. In Victron's own GX interface code the virtual battery is the service com.victronenergy.battery.lynxparallel<N>, where N is the shared VE.Can instance, and it carries two paths the physical BMSes don't: /NumberOfBmses (connected out of total, e.g. 2/2) and the summed /Capacity.

Rules that keep your code honest:

  1. Read bank-level values from the virtual battery. Its state of charge is capacity-weighted and its limits follow the combination rules in Part 4. Re-averaging the physical BMSes yourself gets SoC wrong whenever the banks differ in size.
  2. Read health from each physical BMS. /State, /ErrorCode, /Io/AllowToCharge, /Io/AllowToDischarge and the min/max cell values tell you which bank has the problem; the virtual battery doesn't.
  3. Discover services; don't hard-code instances. The virtual battery is the one with /NumberOfBmses; physical BMSes publish charge limits (/Info/MaxChargeCurrent) but not /NumberOfBmses; SmartShunts and BMVs publish neither.
  4. Expect the virtual battery to vanish. If the VE.Can link to the GX breaks, it disappears while the banks keep running. A BMS waiting to join shows a pending state, not an error.
  5. Prefer MQTT for parallel systems. Every service is there with no unit-ID mapping to worry about. Over Modbus, read each BMS on the unit ID the GX lists for it, and check Available services before relying on the virtual battery having a unit ID at all.

Example: every BMS and the virtual bank over MQTT

Python with paho-mqtt 2.x (pip install "paho-mqtt>=2"). We tested it against a local broker and a simulated GX publishing two BMSes, their virtual bank and a SmartShunt.

import json
import threading
import time

import paho.mqtt.client as mqtt

GX_HOST = "192.168.1.50"
PORTAL_ID = "c0619ab12345"  # Settings → VRM online portal → VRM Portal ID

batteries = {}  # device instance -> {D-Bus path: value}
lock = threading.Lock()


def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe(f"N/{PORTAL_ID}/battery/#")
    client.publish(f"R/{PORTAL_ID}/keepalive", "")  # ask for a full publish


def on_message(client, userdata, msg):
    _, _, _, instance, *path = msg.topic.split("/")
    with lock:
        if not msg.payload:  # empty payload: the device left the GX
            batteries.pop(instance, None)
            return
        batteries.setdefault(instance, {})["/" + "/".join(path)] = json.loads(msg.payload)["value"]


def keep_alive(client):
    # The GX stops publishing 60 s after the last keepalive; refresh every 30 s.
    while True:
        time.sleep(30)
        client.publish(f"R/{PORTAL_ID}/keepalive",
                       json.dumps({"keepalive-options": ["suppress-republish"]}))


def snapshot():
    """Split battery services into the virtual bank(s) and the physical BMSes."""
    with lock:
        services = {i: dict(v) for i, v in batteries.items()}
    # Only BMSes publish charge limits — this skips SmartShunts and BMVs.
    bms = {i: s for i, s in services.items() if "/Info/MaxChargeCurrent" in s}
    virtual = {i: s for i, s in bms.items() if "/NumberOfBmses" in s}
    physical = {i: s for i, s in bms.items() if i not in virtual}
    return virtual, physical


def main(host=GX_HOST, port=1883, settle_s=5):
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
    client.on_connect = on_connect
    client.on_message = on_message
    client.connect(host, port)
    client.loop_start()
    threading.Thread(target=keep_alive, args=(client,), daemon=True).start()
    time.sleep(settle_s)  # let the full publish arrive

    virtual, physical = snapshot()
    for inst, s in virtual.items():
        print(f"BANK (virtual, {s['/NumberOfBmses']} BMSes) instance {inst}: "
              f"{s.get('/Soc')} %  {s.get('/Dc/0/Current')} A  "
              f"CCL {s.get('/Info/MaxChargeCurrent')} A  DCL {s.get('/Info/MaxDischargeCurrent')} A")
    for inst, s in physical.items():
        print(f"  BMS instance {inst}: {s.get('/Soc')} %  {s.get('/Dc/0/Current')} A  "
              f"state {s.get('/State')}  error {s.get('/ErrorCode')}  "
              f"cells {s.get('/System/MinCellVoltage')}-{s.get('/System/MaxCellVoltage')} V")
    return client, virtual, physical


if __name__ == "__main__":
    main()

On a two-bank system the output looks like this (values from our simulation):

BANK (virtual, 2/2 BMSes) instance 0: 80.0 %  -118.0 A  CCL 500.0 A  DCL 1000.0 A
  BMS instance 512: 82.0 %  -60.0 A  state 9  error 0  cells 3.28-3.3 V
  BMS instance 513: 78.0 %  -58.0 A  state 9  error 0  cells 3.27-3.29 V

The GX broker no longer retains messages: nothing arrives until you send a keepalive, and publishing stops 60 seconds after the last one. A value that becomes invalid arrives as null; a device that disappears sends an empty payload on each of its topics.


The mistakes that cost money

  1. Pairing the wrong BMS and battery. Lynx Smart BMS NG is for Lithium NG; the Lynx Smart BMS without "NG" is for Lithium Smart.
  2. Mixing a 500 A and a 1000 A BMS in a parallel bank. Not supported.
  3. Unequal load-side cables between paralleled BMSes and the busbar. The banks stop sharing current evenly.
  4. Paralleling AUX outputs without blocking diodes.
  5. Counting on redundancy without designing for N−1. One BMS out means its share of the current limit is gone.
  6. Reading power from register 258. It clips at 32,767 W.
  7. Using the device instance as the Modbus unit ID. Take it from the GX's list.
  8. Assuming the virtual battery always exists in code. It disappears when the GX loses the BMSes.

Sources

Checked on 22 October 2026:

We are not affiliated with Victron Energy.


Want exact numbers for your boat?

Use the configurator to generate a vendor-ready spec sheet.

Start the configurator
TPublished by TMHOctober 22, 2026
About TMH & methodology