ESPHome component to monitor EG4 Battery Management Systems (EG4 BMS) via RS485/Modbus RTU.
This is a native C++ implementation that communicates directly with the EG4 BMS using the Modbus RTU protocol, providing better stability and lower resource usage compared to using the built-in ESPHome Modbus controller.
- EG4 LL (Lifeline) series batteries
- EG4-LL-S V2 (formerly known as EG4-LL)
- Any EG4 battery with RS485/Modbus RTU interface
- Native Modbus RTU implementation - Direct communication without external dependencies
- Comprehensive monitoring - Voltage, current, temperature, capacity, and more
- Individual cell voltages - Monitor all 16 cells independently
- Status reporting - Real-time status, warnings, protection, and errors
- Multi-BMS support - Connect multiple BMSes on the same RS485 bus
- Low resource usage - Efficient C++ implementation
- Home Assistant integration - Automatic device discovery and configuration
- ESP32 or ESP8266 board
- RS485 to TTL converter module (e.g., MAX485, SP3485)
- EG4 BMS with RS485 port
RS485 UART
┌────────────┐ ┌──────────┐ ┌─────────┐
│ │ │ │<----- RX ----->│ │
│ EG4 │<-----B- ---->│ RS485 │<----- TX ----->│ ESP32/ │
│ BMS │<---- A+ ---->│ to TTL │<----- GND ---->│ ESP8266 │
│ │<--- GND ---->│ module │<----- 3.3V --->│ │
│ │ │ │ │ │
└────────────┘ └──────────┘ └─────────┘
Important: Power the RS485 module with 3.3V to match the ESP's logic level.
| Pin | Signal | Wire Color (T-568B) |
|---|---|---|
| 1 | RS485 B | Orange/White |
| 2 | RS485 A | Orange |
| 3 | GND | Green/White |
| 4 | - | Blue |
| 5 | - | Blue/White |
| 6 | GND | Green |
| 7 | RS485 A | Brown/White |
| 8 | RS485 B | Brown |
The RS485 pair is bridged across pins 1/8 (B) and 2/7 (A), so either end of the connector works. CAN bus for inverter closed-loop is on a separate port — not on this connector.
Add to your ESPHome YAML configuration:
external_components:
- source: github://rar/esphome-eg4-bms@main
refresh: 0s- Clone this repository
- Copy the
componentsfolder to your ESPHome configuration directory - Reference it in your YAML:
external_components:
- source: componentsuart:
id: uart_0
baud_rate: 9600
tx_pin: GPIO16
rx_pin: GPIO17
rx_buffer_size: 256
eg4_modbus:
id: modbus0
uart_id: uart_0
eg4_bms:
id: bms0
address: 0x10 # Default address for master BMS
eg4_modbus_id: modbus0
update_interval: 10s
sensor:
- platform: eg4_bms
eg4_bms_id: bms0
total_voltage:
name: "Battery Voltage"
current:
name: "Battery Current"
state_of_charge:
name: "Battery SOC"See the example configurations in this repository:
- esp32-example.yaml - Full configuration for ESP32
- esp32s3-example.yaml - Full configuration for ESP32-S3
- esp8266-example.yaml - Minimal configuration for ESP8266
| Variable | Type | Default | Description |
|---|---|---|---|
id |
Required | - | ID of the Modbus component |
uart_id |
Required | - | ID of the UART component |
flow_control_pin |
Optional | - | GPIO pin for RS485 flow control (DE/RE) |
| Variable | Type | Default | Description |
|---|---|---|---|
id |
Required | - | ID of the BMS component |
eg4_modbus_id |
Required | - | ID of the parent Modbus component |
address |
Optional | 0x10 |
Modbus address of the BMS (0x01-0xF7; EG4 uses 0x01-0x10) |
update_interval |
Optional | 10s |
How often to poll the BMS |
max_no_response_count |
Optional | 5 |
Consecutive polls without a valid data block before the BMS is marked offline |
Some EG4 LL-V2 units intermittently return a complete, CRC-valid function-03 response whose entire register data area is zero. Accepting those frames publishes false zeroes for SOC, pack voltage and capacity, which can knock a battery out of a downstream aggregate such as YamBMS.
Each register block is therefore validated before anything is published, and rejected as a unit if it is implausible:
- Electrical/cell block - rejected if the payload is entirely zero or the pack voltage reads 0.00 V.
- Capacity/SOC block - rejected if the payload is entirely zero or remaining capacity, full capacity and SOH all read zero.
A rejected block publishes nothing, so all previous values are retained, and it
does not reset the online tracker. Legitimate zeroes are unaffected: SOC = 0
on a discharged pack and max_charge_current = 0 on a charge-limited pack are
still accepted as long as the rest of the block is plausible.
Expose the rejected_frame_count sensor to monitor how often this happens; the
rejected frame is also logged as hex at WARN level.
- Master BMS: Address
0x10(default) - Battery Module 1: Address
0x01 - Battery Module 2: Address
0x02 - ...
- Battery Module 15: Address
0x0F
Use DIP switches on the BMS to configure the address.
online_status- BMS communication statusheating- Battery heating activecharging- Charging is permitted (no charge-blocking protection active)discharging- Discharging is permitted (no discharge-blocking protection active)
Note that charging and discharging report whether the BMS permits each
direction, derived from the protection bits - not whether current is currently
flowing. An idle battery reports both as ON. Use current or status to tell
what the pack is actually doing.
Voltage:
total_voltage- Total battery pack voltage (V)min_cell_voltage- Minimum cell voltage (V)max_cell_voltage- Maximum cell voltage (V)delta_cell_voltage- Voltage difference between min and max cells (V)min_voltage_cell- Index (1-16) of the lowest cellmax_voltage_cell- Index (1-16) of the highest cellcell_average_voltage- Average cell voltage (V)cell_voltage_1tocell_voltage_16- Individual cell voltages (V)
Current and Power:
current- Battery current (A, positive = charging, negative = discharging)power- Battery power (W)charging_power- Charging power (W)discharging_power- Discharging power (W)
Temperature:
pcb_temperature- PCB temperature (°C)avg_temperature- Average temperature (°C)max_temperature- Maximum temperature (°C)temperature_1totemperature_6- Individual temperature sensors (°C)
Capacity:
remaining_capacity- Remaining capacity (Ah)full_capacity- Full capacity (Ah)
State:
state_of_charge- State of charge (%)state_of_health- State of health (%)cycle_count- Charge/discharge cyclesmax_charge_current- Maximum charging current (A)cell_count- Number of cells reporting a valid voltage
Diagnostic:
errors_bitmask- Raw error register valuewarnings_bitmask- Raw warning register valueprotection_bitmask- Raw protection register valuerejected_frame_count- Count of implausible register blocks rejected since boot
status- Battery status (Standby, Charging, Discharging, etc.)warnings- Active warningsprotection- Active protectionserror- Error codesmodel- BMS model numberfirmware_version- BMS firmware versionserial_number- BMS serial number
- Standby - Battery is idle
- Charging - Battery is charging
- Discharging - Battery is discharging
- Protect - Protection is active
- Charging Limited - Charging current is limited
- Heating+[Status] - Battery heating is active
- Pack OV - Pack overvoltage
- Cell OV - Cell overvoltage
- Pack UV - Pack undervoltage
- Cell UV - Cell undervoltage
- Charge OC - Charge overcurrent
- Discharge OC - Discharge overcurrent
- Abnormal Temp - Abnormal ambient temperature
- MOS Overheat - MOSFET overheating
- Charge OT - Charge over-temperature
- Discharge OT - Discharge over-temperature
- Charge UT - Charge under-temperature
- Discharge UT - Discharge under-temperature
- Low Capacity - Low capacity warning
- Float Stopped - Float charging has stopped (warning only)
- Discharge SC - Discharge short circuit (protection only)
- Voltage Error - Voltage sensor error
- Temperature Error - Temperature sensor error
- Current Flow Error - Current sensor error
- Cell Unbalance - Cell voltage unbalance error
Enable debug logging to see communication details:
logger:
level: DEBUG
logs:
eg4_modbus: DEBUG
eg4_bms: DEBUG
uart:
id: uart_0
baud_rate: 9600
tx_pin: GPIO16
rx_pin: GPIO17
debug:
direction: BOTH
dummy_receiver: false
after:
timeout: 50ms
sequence:
- lambda: UARTDebug::log_hex(direction, bytes, ':');- Check wiring - Verify A+, B-, and GND connections
- Check baud rate - EG4 BMS default is 9600
- Check address - Make sure the address matches your BMS configuration
- Check power - RS485 module should be powered with 3.3V
- Check termination - Long cable runs may need 120Ω termination resistors
- Check register mapping - Different BMS models may have different register layouts
- Update firmware - Ensure your BMS has the latest firmware
- Check polling interval - Don't poll too frequently (minimum 1 second recommended)
- Check cable quality - Use shielded twisted pair cable for RS485
- Check cable length - Keep cables as short as possible (max 1200m for RS485)
- Check termination - Add 120Ω termination resistors at both ends
This component implements the EG4 Battery Modbus RTU protocol as documented in the EG4-LL-MODBUS-Communication-Protocol documentation.
Protocol Details:
- Type: Modbus RTU
- Function Code: 0x03 (Read Holding Registers)
- Baud Rate: 9600 (default), configurable up to 19200
- Data Bits: 8
- Stop Bits: 1
- Parity: None
Advantages:
- ✅ Simpler configuration - No need to manually configure each register
- ✅ Better stability - Native implementation with proper error handling
- ✅ Lower resource usage - More efficient memory and CPU usage
- ✅ Automatic calculations - Delta voltage, power, etc. calculated automatically
- ✅ Status decoding - Human-readable status, warnings, and errors
- ✅ Better Home Assistant integration - Proper device classes and units
Migration from Modbus Controller: Simply replace your modbus_controller configuration with this component. All sensors will be available with clearer naming and better organization.
Contributions are welcome! Please feel free to submit pull requests or open issues.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- Inspired by esphome-seplos-bms by syssi
- Based on the EG4 Modbus protocol documentation
- Original Modbus controller implementation from esphome-yambms
Breaking changes
designed_capacityhas been removed. It was configurable but never populated and has no known register; remove it from your YAML.addressis now validated to 0x01-0xF7, rejecting the 0x00 broadcast address.
Changes
- Reject CRC-valid but all-zero register blocks instead of publishing false zeroes, retaining the last good values (#7)
- Only reset the online tracker on a plausible data block
- Add the
rejected_frame_countdiagnostic sensor - Make the missed-update threshold configurable via
max_no_response_count - Fix
average_cell_voltagein the ESP32 examples and docs; the schema key iscell_average_voltage - Saturate the missed-update counter instead of letting it wrap, which flapped a long-dead BMS back online every 256 polls
- Drop
device_class: energyfrom the Ah capacity sensors, which Home Assistant rejects as a unit mismatch and excludes from long-term statistics - Publish
cell_count(previously configurable but never populated) - Remove
designed_capacity, which was never populated and has no known register - Text-sensor blocks no longer mark a BMS online; only a valid data block does, so a unit returning zeroed measurements can no longer be kept online by its model-string reply
- Reject Modbus addresses outside 0x01-0xF7 and log an error at startup when two devices on one bus share an address
- Mark the bitmask and cell-index sensors as diagnostic entities
- Correct the register map in PROJECT_SUMMARY.md, document the missing sensors,
clarify that
charging/dischargingmean "permitted" rather than "active", and fix the UART debug snippet, which never triggered on binary Modbus frames - Set device class battery for SoC and SoH sensors (#6)
- Fix the BMS RJ45 pinout in the docs; RS485 is on pins 1/2 and 7/8, not 4/5 (#4)
- Initial release
- Native Modbus RTU implementation
- Support for all EG4 LL series batteries
- Comprehensive sensor suite
- Multi-BMS support
For issues, questions, or feature requests, please open an issue on GitHub.