Monitor every cell in a 2 V battery bank with an ESP32
Big lead-acid banks — forklift batteries, telecom strings, off-grid solar — are built from 2 V cells wired in series. One weak cell drags the whole bank down, and a pack voltmeter won't show it until it's too late. This build measures every cell individually and sends the readings to Home Assistant, so you see a cell falling behind weeks before it fails.
The example is an 18-cell, 36 V bank, but the generator below builds the config for anything from 2 to 32 cells.
Battery banks can deliver thousands of amps. A sense wire that rubs through and touches the wrong post can weld, burn or explode a cell. Fuse every sense wire right at the battery post, use insulated tools, and remove rings and watches. Flooded cells vent hydrogen while charging: keep the electronics out of the space directly above the cells and never make or break connections near the vents during or just after charging.
How it works
Every junction between two cells is a tap. With 18 cells there are 18 taps above the pack's negative terminal: tap 1 is the top of cell 1, tap 18 is pack positive. The monitor measures each tap's voltage against pack negative, then subtracts neighbors to get each cell:
cell 1 = tap 1
cell 2 = tap 2 − tap 1
…
cell 18 = tap 18 − tap 17
Tap 18 can reach about 45 V during an equalize charge, far too much for any chip input. So each tap goes through a resistor divider that scales it down by the same ratio, and an ADS1115 converter reads the result with 16-bit precision. The ESP32's own analog inputs aren't accurate enough to see the 20–50 mV differences that matter between 2 V cells.
Each ADS1115 has four inputs and four possible I2C addresses, so one I2C bus holds 16 inputs. Eighteen cells need five boards: four on the first bus and one on a second bus, which the ESP32 supports.
Parts
- An ESP32 DevKit.
- ADS1115 boards: one per four cells, rounded up (five for 18 cells).
- One divider per cell: a 1 MΩ and a 68 kΩ resistor, 1% metal film or better.
- A DC-DC buck converter rated for at least 60 V input with 5 V output, to run the ESP32 from the bank itself.
- Inline fuse holders with 1 A fuses: one per sense wire, plus one for the converter.
- Ring terminals sized for your cell posts, 22–18 AWG wire, and a terminal block for the sense wires.
- Optional: a DS18B20 waterproof temperature probe for a pilot cell, and a current shunt (for example 500 A / 50 mV) if you want charge and discharge current.
- A closed plastic enclosure. Acid mist corrodes bare circuit boards.
- ESP32 DevKitView on AmazonPhoto: Edwiyanto, CC BY-SA 4.0

- ADS1115 16-bit ADC board
- 1 MΩ and 68 kΩ 1% resistorsPhoto: Afrank99, CC BY-SA 2.5

- 60 V-input 5 V buck converter
- Inline fuse holder
- Insulated ring terminals
- DS18B20 waterproof probe
- 500 A / 50 mV current shunt
- Plastic project enclosure
Why one divider value for every tap
Using the same 1 MΩ / 68 kΩ divider on every tap, even the bottom ones that only see 2 V, is deliberate. If a sense wire ever lands on the wrong post — even pack positive — that input still can't be pushed past the ADS1115's 3.3 V limit until the bank exceeds about 50 V. A mistake shows up as a strange reading instead of a burned chip.
The high resistor values keep the drain tiny: about 0.3 mA total. Because the bottom cell feeds every divider and the top cell only one, they don't drain evenly. That's irrelevant while a charger is connected, but if the bank will sit unused for months, pull the sense fuses.
Wiring
- Ground first. The ESP32, every ADS1115 and the buck converter's output share one ground: pack negative. Run it from the battery side of any shunt.
- Power. Fused pack positive and pack negative go to the buck converter's input. Set its output to 5 V before connecting the ESP32's VIN.
- Sense wires. One wire from each cell's positive post, with a fuse within a few centimeters of the post, to the terminal block in the enclosure. Label both ends by tap number.
- Dividers. At the terminal block, each tap goes through its 1 MΩ resistor to an ADS1115 input, with the 68 kΩ resistor from that input to ground.
- ADS1115 boards. All run from 3.3 V. Taps 1–4 go to board 1's A0–A3, taps 5–8 to board 2, and so on. Set each board's address with its ADDR pin: GND for 0x48, VDD for 0x49, SDA for 0x4A, SCL for 0x4B. Boards 1–4 go on the first I2C bus (GPIO21/22), board 5 onward on the second (GPIO32/33).
Flash the ESP32 over USB before wiring it to the bank, then do all later updates over Wi-Fi. The board's ground is tied to pack negative, and plugging a computer into it while it's connected joins your PC's ground to the battery system.
Generate your config
Set your cell count and options. The YAML updates as you go, with every tap, every cell, and summary sensors for pack voltage, lowest, highest and average cell, and the spread between them.
Your config
The generated config passes ESPHome's validator for every size from 2 to 32 cells. It includes a fresh API encryption key and OTA password, made in your browser.
Calibrate it
Even with 1% resistors, each tap can read a few percent off, which is more than 1 V at the top of the bank — useless for cell work until it's calibrated. It only takes one pass with a good multimeter:
- In the config, temporarily remove the
internal: trueline from each tap so the tap readings appear in Home Assistant. - With the bank resting (no charge or load for at least an hour), measure each tap with your meter: black probe on pack negative, red on that tap's post.
- For each tap, work out a new factor:
new multiply = old multiply × meter reading ÷ Home Assistant reading. Put it in that tap'smultiplyline. - Reinstall, check a few taps again, then put the
internal: truelines back.
Done properly, cell readings land within about 5 mV of the meter. Recheck once a year; resistors drift slightly with age and heat.
Reading the numbers
These are typical values for flooded lead-acid cells at about 20–25 °C. Sealed AGM and gel cells differ a little, so check your battery maker's sheet for charge settings.
| Per cell | 18-cell bank | Meaning |
|---|---|---|
| 2.12 V | 38.2 V | About 100% charged, at rest |
| 2.08 V | 37.5 V | About 80%, at rest |
| 2.03 V | 36.6 V | About 50%, at rest. Recharge soon. |
| 1.98 V | 35.7 V | About 20%, at rest. Recharge now. |
| 2.23–2.25 V | 40.1–40.5 V | Typical float charge |
| 2.30–2.35 V | 41.4–42.3 V | Typical absorption charge |
"At rest" means no charge or load for several hours; under load or charge, voltages don't map to state of charge. Charge voltages also change with temperature — roughly 4–5 mV per cell for every °C away from 20–25 °C, lower when warm and higher when cold — which is what the pilot cell probe is for.
What to watch
- Cell spread on float. On a healthy bank in float, cells sit within a few hundredths of a volt of each other. A spread that grows past about 0.05 V, or one cell that's always lowest, is the early warning.
- The same low cell every time. The Lowest cell number sensor shows which one. If it's the same cell week after week, check its specific gravity with a hydrometer and its connections for corrosion before suspecting the cell itself.
- A cell diving under load. A cell that drops much faster than the rest during discharge has lost capacity. Equalizing may help a flooded cell; if not, it's the one to replace.
Home Assistant alerts
- Imbalance: notify when Cell spread stays above 0.05 V for an hour while the bank is on float.
- Low cell: notify when Lowest cell drops below 1.95 V. During a heavy discharge, a common cutoff is about 1.75 V per cell; going lower damages lead-acid cells.
- Monitor offline: notify when Pack voltage becomes unavailable for 10 minutes. A blown sense fuse or a dead converter should never go unnoticed.
A history graph of all the cells on one chart is the most useful dashboard card: healthy cells move together, and a failing one peels away from the group.