| `modbus_per_pack`| Modbus RTU, fn 0x03 | 9600 | master (per pack)| **Primary path for LP4V2 Auto-Addressing.** One FTDI per pack's RS485 port, polls once per cycle, fully decodes into named HA entities. |
| `active` | EG4 7E/0D legacy | 9600 | master (shared) | **Legacy.** V1 firmware only. V2 packs don't respond to this protocol — kept for reference. |
| `passive` | Modbus RTU sniff | 19200 | listener | **Diagnostic.** Originally intended to listen on an LVX6048 BMS bus; the LVX6048 doesn't poll EG4 packs that way, so has no production use here. |
## How we got here (summary)
1.**Port matrix test** (2026-04-24). Established that the LP4V2 back panel's four RJ45s (CAN / RS485 / Comm1 / Comm2) carry three distinct electrical buses:
- Comm1 + Comm2 = inter-pack hub bus (19200 Modbus, master-to-slave coordination). Pin 1-2 and pin 7-8 both tap the same bus.
- RS485 = external monitor bus. Inactive until an external master drives it.
- CAN = separate bus, not in scope.
2.**EG4 BMS Tool capture.** User confirmed the stock Windows/macOS BMS Tool connects to the RS485 port at 9600 baud, Modbus slave ID **0x40**. Our first canonical Modbus probe at that address returned a clean 99-byte reply.
3.**`lv_host.app` reverse-engineering.** The BMS Tool is a Qt app. Its Mach-O binary contains:
- SQLite schema for the `total` table → complete list of 39 fields the BMS Tool stores per cycle.
- String-table names for warning / protection bit flags.
- C++ symbols `BmsDatalog::allFunctionModbusAnalysis`, `BmsMonitoring::allFunctionModbusAnalysis`, `usModbusAskRegBase` etc. → confirms Modbus RTU fn 0x03 read-holding-regs.
4.**Register map construction.** Correlated live register values (cell voltages, pack V) against the SQL field list to derive the 47-reg map below. High-confidence fields promoted to named HA entities; unknowns still emitted as `register_NN` for future correlation.
**Confidence levels**: Bold-worthy certain (confirmed by live values + UI labels): pack_voltage, cells 01-16, SOC, SOH, cell_count, capacity_ah. Medium (fits data, unverified): temps, current, bitfields, Battery_Mode. Unknown: regs 32, 35 (probably Error_Code but value always 0 so far), 38-40, 43-45.
## Warning / protection bit maps
From the UI labels in lv_host.app (bit 0 = first listed):
| Bit | Warning (reg 33) | Protection (reg 34) |
|-----|-------------------|---------------------|
| 0 | pack_ov | pack_ov |
| 1 | cell_ov | cell_ov |
| 2 | pack_uv | pack_uv |
| 3 | cell_uv | cell_uv |
| 4 | charge_oc | charge_oc |
| 5 | discharge_oc | discharge_oc |
| 6 | temp_anomaly | temp_anomaly |
| 7 | mos_ot | mos_ot |
| 8 | charge_ot | charge_ot |
| 9 | discharge_ot | discharge_ot |
| 10 | charge_ut | charge_ut |
| 11 | discharge_ut | discharge_ut |
| 12 | low_capacity | float_stopped |
| 13 | other_error | discharge_sc |
Each bit becomes an HA sensor reporting `on` / `off`. Exact bit ordering is guessed from UI display order — adjust if the EG4 tool ever shows a flag we don't match.
## Hardware topology notes
### Critical: RS485 port only works when the pack is standalone
**Empirical finding** (2026-04-24): the LP4V2's external `RS485` port
only answers Modbus queries when the pack is **not** daisy-chained to
other packs via `Comm1`/`Comm2`. Specifically:
- With daisy chains intact (bat1 Comm2 → bat2 Comm1 etc.), one pack
elects as master and polls slaves over the internal hub bus
(19200 Modbus on Comm1/Comm2). Slave packs' RS485 ports go silent —
only the master responds externally.
- Remove the inter-pack Comm jumpers and each pack becomes a
self-contained master: its Comm1/Comm2 LEDs flash (it's trying to
poll slaves that aren't there), and its own RS485 port becomes fully
| Comm1 | Inter-pack hub bus (in/out) | 1-2 and 7-8 both tap it | Modbus RTU @ 19200 |
| Comm2 | Inter-pack hub bus (in/out) | 1-2 and 7-8 both tap it | Modbus RTU @ 19200 |
- The **stock USB-RS485 cable** ships wired to pins 1-2 — usable on either Comm or RS485.
- The **pin 7-8 modified cable** only gains us the Comm/Comm2 hub-bus tap; since pins 1-2 reach the same bus, it's not strictly necessary. Kept in the toolkit for diagnostic purposes.
- Factory inter-pack jumpers (between packs) are 8-conductor CAT5 — they carry both pin pairs.
### Adapters
On this host, three USB-FTDI adapters are plugged into the three packs' RS485 ports:
Each pack gets polled on its own bus → no shared-bus arbitration, no master/slave coordination needed, pack Modbus address is 0x40 for all of them.
## LVX6048 compatibility (still true)
LVX6048 BMS port protocols: `PYL` (Pylontech), `LIb` (MPP LIO), `WEC` (WECO), `SOL` (Soltaro), `VSC` (Pylontech-CAN), `USE` (voltage-only). **No native EG4 LP4V2 support.** For inverter↔battery comms, set `P05/P14 = USE` and manage charge profile via `lvx-flash`. See DIY Solar Forum threads 67496 & 96019, LVX6048WP manual §9-2.
## Bring-up checklist (when a new pack goes live)
1. Wire: plug USB-FTDI adapter (stock pin-1-2 cable) into the pack's **RS485** port.
2. Confirm the pack's BMS is powered (LEDs steady on Comm1 + Comm2, not dark).
3. Verify the `/dev/serial/by-id/...` symlink exists for the adapter.
4. Add a pack entry to the config:
```yaml
packs:
- name: lifepower4_N
address: 0x40
port: /dev/serial/by-id/usb-FTDI_...-if00-port0
baud: 9600
```
5.`sudo systemctl restart eg4-battery.service`. Watch journal — within ~10 s you should see the first MQTT publish, or `WARNING: no/bad response` if the pack isn't answering.
6. In HA: `EG4 LifePower4 lifepower4_N` device appears with ~65 auto-discovered entities.