161 lines
11 KiB
Markdown
161 lines
11 KiB
Markdown
|
|
# EG4 LifePower4 v2 — architecture & protocol notes
|
|||
|
|
|
|||
|
|
## Modes — choose per deployment
|
|||
|
|
|
|||
|
|
`bus.mode` in the config picks one of three daemon modes:
|
|||
|
|
|
|||
|
|
| Mode | Wire protocol | Baud | Role | Status |
|
|||
|
|
|------------------|----------------------|-------|----------------|------------------------------------------------------------|
|
|||
|
|
| `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.
|
|||
|
|
|
|||
|
|
## Register map (Modbus fn 0x03, start 0x0000, count 47)
|
|||
|
|
|
|||
|
|
From live observation + lv_host.app schema:
|
|||
|
|
|
|||
|
|
| Reg | Observed | Field | Scale | Unit | HA entity suffix |
|
|||
|
|
|-------|--------------|---------------------|---------|------|----------------------|
|
|||
|
|
| 00 | 5256 | Total_Voltage | × 0.01 | V | `pack_voltage` |
|
|||
|
|
| 01 | 0 (signed) | Current_I | × 0.01 | A | `pack_current` |
|
|||
|
|
| 02-17 | ~3285 each | Vol_Cell01..16 | × 0.001 | V | `cell_01_voltage`..`cell_16_voltage` + `cell_voltage_min/max/delta_mv/lowest/highest` |
|
|||
|
|
| 18 | 21 | Temp_01 | × 1 | °C | `temperature_01` |
|
|||
|
|
| 19 | 21 | Temp_02 | × 1 | °C | `temperature_02` |
|
|||
|
|
| 20 | 20 | Temp_03 | × 1 | °C | `temperature_03` |
|
|||
|
|
| 21 | 54 | Temp_04 | × 1 | °C | `temperature_04` |
|
|||
|
|
| 22 | 100 | SOC | × 1 | % | `soc` |
|
|||
|
|
| 23 | 100 | SOH | × 1 | % | `soh` |
|
|||
|
|
| 24 | 55 | Temp_PCB | × 1 | °C | `temperature_pcb` |
|
|||
|
|
| 25-29 | 0 | reserved | | | (register_NN only) |
|
|||
|
|
| 30 | 1 | Heater (bit 0) | | | `heater` (on/off) |
|
|||
|
|
| 31 | 5493 | MAX_Curren | × 0.01 | A | `max_current_limit` |
|
|||
|
|
| 32 | 10752 | *?* | | | (register_32) |
|
|||
|
|
| 33 | ? | Warning bitfield | | | `warning_*` (14 bits)|
|
|||
|
|
| 34 | ? | Protection bitfield | | | `protection_*` (14 bits)|
|
|||
|
|
| 35 | 0 | Error_Code | | | `error_code` |
|
|||
|
|
| 36 | 16 | Cell_Num | | | `cell_count` |
|
|||
|
|
| 37 | 1000 | Capacity | × 0.1 | Ah | `capacity_ah` |
|
|||
|
|
| 38 | 0 | Remaining | × 0.01 | Ah | `remaining_ah` |
|
|||
|
|
| 39 | 0 | CycleNum | | | `cycle_count` |
|
|||
|
|
| 40 | 7 | Battery_Mode (enum) | | | `battery_mode` |
|
|||
|
|
| 41 | 0x0fff | BMS_Version (hi) | | | `bms_version_hi` |
|
|||
|
|
| 42 | 0x07ff | BMS_Version (lo) | | | `bms_version_lo` |
|
|||
|
|
| 43-45 | — | *?* | | | (register_NN only) |
|
|||
|
|
| 46 | +1.25 Hz | runtime counter | × 0.1 s?| | `uptime_ds` |
|
|||
|
|
|
|||
|
|
**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
|
|||
|
|
live for external queries.
|
|||
|
|
|
|||
|
|
**Implication**: `modbus_per_pack` mode requires **each pack standalone**
|
|||
|
|
— one FTDI adapter per pack's RS485 port, no inter-pack Comm jumpers.
|
|||
|
|
This is how we got bat1 responding cleanly. If the batteries later
|
|||
|
|
need to be daisy-chained to an inverter, only the master pack's RS485
|
|||
|
|
port will answer external queries; slave per-pack data would need to
|
|||
|
|
come from decoding the Comm1/Comm2 hub bus instead (a future mode).
|
|||
|
|
|
|||
|
|
### Port roles on the LP4V2 pack (from the port matrix test)
|
|||
|
|
|
|||
|
|
| Port | Role | Pins carrying signal | Protocol / baud |
|
|||
|
|
|-------|--------------------------------|-------------------------|-----------------------|
|
|||
|
|
| CAN | Inverter CAN comms | (CAN-specific pinout) | CANbus (not RS-485) |
|
|||
|
|
| RS485 | External monitor | 1-2 | Modbus RTU @ 9600 |
|
|||
|
|
| 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:
|
|||
|
|
|
|||
|
|
| Adapter ID | Pack | `/dev/serial/by-id/...` |
|
|||
|
|
|------------------|----------------|--------------------------------------------------------|
|
|||
|
|
| A994XMVK | bat1 (RS485) | `usb-FTDI_FT232R_USB_UART_A994XMVK-if00-port0` |
|
|||
|
|
| A994XGUY | bat2 (RS485) | `usb-FTDI_FT232R_USB_UART_A994XGUY-if00-port0` |
|
|||
|
|
| A994XMBR | bat3 (RS485) | `usb-FTDI_FT232R_USB_UART_A994XMBR-if00-port0` |
|
|||
|
|
|
|||
|
|
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.
|
|||
|
|
|
|||
|
|
## Sources / references
|
|||
|
|
|
|||
|
|
- `lv_host.app` (Qt) — Contents/MacOS/lv_host Mach-O binary + my1.db/my2.db SQLite schemas
|
|||
|
|
- `../battery/eg4_lifepower.py` — V1 7E/0D decoder (Louisvdw/dbus-serialbattery port), historical reference
|
|||
|
|
- `../battery/sweep.py` — protocol + baud scanner used for initial triage
|
|||
|
|
- EG4 "Cables Needed for Updating" PDF
|
|||
|
|
- EG4 Community Forum: "Specs for LifePower4 V2 BAT-COM ports"
|
|||
|
|
- LVX6048WP manual §9-2 (BMS pinout), §Programs P03/P05
|