INA219 Current & Energy Meter: What Your Device Really Consumes
Put an INA219 in series with the power line and watch voltage, current, power and accumulated mWh in your browser. Indispensable for estimating battery life.
Why
A multimeter shows instantaneous current, but an ESP32 device draws power in bursts: 150 mA for half a second during Wi-Fi, then 10 µA asleep. To honestly estimate battery life you need to integrate consumption over time. That's what this meter does: it polls the INA219 hundreds of times a second and accumulates energy in mWh.
Parts
- ESP32 DevKit (the meter)
- INA219 module (0.1 Ω shunt: up to 26 V and ±3.2 A)
- The device under test and its power source
Wiring
The INA219 goes in series with the positive wire of the load:
Source + ──► VIN+ [INA219] VIN- ──► + Load
Source − ──────┬──────────────────► − Load
└── GND INA219 ── GND ESP32
The source negative, the module's GND and the ESP32's GND must be tied together — otherwise the INA219 can't measure voltage.
| INA219 | ESP32 |
|---|---|
| VCC / GND | 3V3 / GND |
| SDA / SCL | GPIO 21 / GPIO 22 |
Library
Adafruit INA219 (along with Adafruit BusIO).
Code
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_INA219.h>
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";
Adafruit_INA219 ina;
WebServer server(80);
float voltage = 0, current = 0, power = 0, peakCurrent = 0;
double energy_mWh = 0;
unsigned long lastSample = 0;
unsigned long startedAt = 0;
const char PAGE[] = R"rawliteral(
<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Power meter</title>
<style>
body{font-family:system-ui;max-width:520px;margin:30px auto;padding:0 16px}
.g{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.c{background:#f1f4f7;border-radius:10px;padding:14px}
.v{font-size:1.8rem;font-weight:700}.l{color:#667;font-size:.9rem}
button{margin-top:16px;padding:10px 16px;font-size:1rem}
</style></head><body>
<h2>Power meter</h2>
<div class="g">
<div class="c"><div class="v" id="V">-</div><div class="l">V</div></div>
<div class="c"><div class="v" id="I">-</div><div class="l">mA</div></div>
<div class="c"><div class="v" id="P">-</div><div class="l">mW</div></div>
<div class="c"><div class="v" id="E">-</div><div class="l">mWh</div></div>
<div class="c"><div class="v" id="K">-</div><div class="l">peak mA</div></div>
<div class="c"><div class="v" id="A">-</div><div class="l">avg mA</div></div>
</div>
<button onclick="fetch('/reset').then(update)">Reset</button>
<script>
async function update() {
const d = await (await fetch('/data')).json();
V.textContent = d.v.toFixed(3); I.textContent = d.i.toFixed(1);
P.textContent = d.p.toFixed(0); E.textContent = d.e.toFixed(2);
K.textContent = d.peak.toFixed(0); A.textContent = d.avg.toFixed(2);
}
update(); setInterval(update, 1000);
</script></body></html>
)rawliteral";
void resetStats() {
energy_mWh = 0;
peakCurrent = 0;
startedAt = millis();
}
void setup() {
Serial.begin(115200);
if (!ina.begin()) {
Serial.println("INA219 not found");
while (true) delay(1000);
}
// More precise for small currents (up to 400 mA): ina.setCalibration_16V_400mA();
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println(WiFi.localIP());
server.on("/", [] { server.send(200, "text/html", PAGE); });
server.on("/reset", [] { resetStats(); server.send(200, "text/plain", "ok"); });
server.on("/data", [] {
double hours = (millis() - startedAt) / 3600000.0;
double avg_mA = (hours > 0 && voltage > 0.1) ? energy_mWh / hours / voltage : 0;
char json[160];
snprintf(json, sizeof(json),
"{\"v\":%.3f,\"i\":%.2f,\"p\":%.1f,\"e\":%.4f,\"peak\":%.1f,\"avg\":%.3f}",
voltage, current, power, energy_mWh, peakCurrent, avg_mA);
server.send(200, "application/json", json);
});
server.begin();
resetStats();
lastSample = millis();
}
void loop() {
server.handleClient();
// Poll ~200 times a second so short peaks aren't missed
unsigned long now = millis();
if (now - lastSample >= 5) {
unsigned long dt = now - lastSample;
lastSample = now;
voltage = ina.getBusVoltage_V() + ina.getShuntVoltage_mV() / 1000.0;
current = ina.getCurrent_mA();
power = voltage * current; // mW
energy_mWh += power * dt / 3600000.0; // mW × h
if (current > peakCurrent) peakCurrent = current;
}
}
How to use it
- Connect the load and open the meter's IP in a browser.
- Press Reset and let the device run for at least a few sleep/wake cycles (say, 30 min).
- avg mA is the average current. Battery life ≈
battery capacity (mAh) / avg mAhours.
Example: the sensor from the "Battery-Powered Sensor" project averaged 0.7 mA → a 2000 mAh cell lasts ~2850 h ≈ 4 months.
Limitations
- An INA219 with a 0.1 Ω shunt resolves about 0.1 mA. It won't see microamp sleep currents — use a dedicated tool (Nordic PPK2, µCurrent) or a 10 Ω shunt for those.
- Sampling every 5 ms misses pulses shorter than a few milliseconds — fine for estimating the average.
- 26 V maximum — never connect it to mains!
Ideas
- Log to CSV on an SD card for charts.
- Compare sleep optimisations: static IP, ESP-NOW, polling rate.
- Monitor a solar panel and battery charging.