Power Management for IoT
Battery-powered IoT devices live or die by their power efficiency. An ESP32 running full-time drains a 2000 mAh battery in about 3 days. With deep sleep, the same battery lasts over 2 years. Understanding power management is what separates prototypes from production devices.
Power Consumption Basics
python
# Battery life calculation formula
def battery_life_days(capacity_mah, avg_current_ma):
"""
Estimate battery life.
capacity_mah — battery capacity in milliamp-hours
avg_current_ma — device average current draw in milliamperes
"""
hours = capacity_mah / avg_current_ma
days = hours / 24
return round(days, 1)
# ESP32 scenarios
scenarios = [
("Always on (WiFi active)", 240, "mA"),
("Active, WiFi off", 30, "mA"),
("Modem sleep", 20, "mA"),
("Light sleep", 0.8, "mA"),
("Deep sleep", 0.010, "mA"), # 10 µA
]
battery_mah = 2000 # Common 18650 cell
print(f"Battery: {battery_mah} mAh\n")
print(f"{'Scenario':<30} {'Current':>10} {'Battery Life':>15}")
print("-" * 60)
for name, current_ma, unit in scenarios:
life = battery_life_days(battery_mah, current_ma)
print(f"{name:<30} {current_ma:>8.3f} mA {life:>12.1f} days")Battery: 2000 mAh
Scenario Current Battery Life
────────────────────────────────────────────────────────
Always on (WiFi active) 240.000 mA 0.3 days
Active, WiFi off 30.000 mA 2.8 days
Modem sleep 20.000 mA 4.2 days
Light sleep 0.800 mA 104.2 days
Deep sleep 0.010 mA 8333.3 daysESP32 Sleep Modes
| Mode | CPU | WiFi/BT | GPIO IRQ | RTC | Current |
|---|---|---|---|---|---|
| Active | Running | Active | Yes | Yes | 240 mA |
| Modem Sleep | Running | Off | Yes | Yes | 20 mA |
| Light Sleep | Paused | Off | Yes | Yes | 0.8 mA |
| Deep Sleep | Off | Off | Wakeup only | Yes | 10 µA |
| Hibernation | Off | Off | Limited | Partial | 5 µA |
Deep Sleep with Timer Wakeup
The most common pattern: wake up, read sensor, send data, go back to sleep.
python
# MicroPython ESP32 — deep sleep with timer wakeup
import machine
import esp32
import dht
import network
import urequests
import time
# Wakeup reason tells us why we woke from sleep
cause = machine.reset_cause()
WAKE_CAUSES = {
machine.PWRON_RESET: "Power-on reset",
machine.HARD_RESET: "Hard reset",
machine.WDT_RESET: "Watchdog reset",
machine.DEEPSLEEP_RESET: "Wake from deep sleep",
machine.SOFT_RESET: "Soft reset",
}
print(f"Wake cause: {WAKE_CAUSES.get(cause, 'Unknown')}")
# ── Read sensor ─────────────────────────────────
sensor = dht.DHT22(machine.Pin(4))
sensor.measure()
temperature = sensor.temperature()
humidity = sensor.humidity()
print(f"T={temperature}°C H={humidity}%")
# ── Connect WiFi and send data ──────────────────
def connect_wifi(ssid, password, timeout=10):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(ssid, password)
deadline = time.time() + timeout
while not wlan.isconnected() and time.time() < deadline:
time.sleep_ms(200)
return wlan.isconnected()
if connect_wifi("MySSID", "MyPassword"):
try:
url = "https://api.thingspeak.com/update"
response = urequests.get(
url,
params={"api_key": "WRITE_KEY",
"field1": temperature,
"field2": humidity}
)
print(f"Sent: entry_id={response.text}")
response.close()
except Exception as e:
print(f"Send failed: {e}")
# ── Enter deep sleep for 5 minutes ─────────────
SLEEP_SECONDS = 300 # 5 minutes
print(f"Sleeping for {SLEEP_SECONDS}s...")
machine.deepsleep(SLEEP_SECONDS * 1000) # Argument is milliseconds
# Execution stops here. Next line never runs.Deep Sleep with External Wakeup (PIR Sensor)
python
# Wake from deep sleep when PIR detects motion
from machine import Pin, deepsleep
import esp32
PIR_PIN = 33 # Must use RTC GPIO (0, 2, 4, 12–15, 25–27, 32–39)
# Configure wakeup source before sleeping
esp32.wake_on_ext0(pin=Pin(PIR_PIN, Pin.IN), level=esp32.WAKEUP_ANY_HIGH)
print("Going to deep sleep. Will wake on motion...")
deepsleep()
# Device wakes when PIR goes HIGH, then restarts from top of fileLight Sleep — Preserve RAM and State
Light sleep is useful when you need to retain RAM contents (unlike deep sleep which clears RAM).
python
# MicroPython — light sleep, wake on pin change
from machine import Pin, lightsleep
# Light sleep preserves RAM — great for keeping state between wakeups
button = Pin(0, Pin.IN, Pin.PULL_UP)
sensor_readings = [] # This list survives light sleep
for i in range(10):
print(f"Reading #{i+1}")
# Simulate sensor reading
sensor_readings.append(24.5 + i * 0.1)
# Light sleep for 60 seconds (or wake on button press)
lightsleep(60_000) # 60,000 ms = 60 seconds
if button.value() == 0:
print("Button woke us up!")
break
print(f"All readings: {sensor_readings}")
# RAM was preserved through all light sleepsPower Sources
Battery Options
| Battery Type | Voltage | Typical Capacity | Rechargeable | Notes |
|---|---|---|---|---|
| AA Alkaline | 1.5 V | 2500–3000 mAh | No | Cheap, widely available |
| 18650 Li-ion | 3.7 V | 2000–3500 mAh | Yes | Most common for IoT |
| LiPo Pouch | 3.7 V | 100–10000 mAh | Yes | Thin, flexible shape |
| LiFePO4 | 3.2 V | 1000–6000 mAh | Yes | Safer, longer cycle life |
| AAA Alkaline | 1.5 V | 1000–1200 mAh | No | Smaller, less capacity |
| CR2032 Coin | 3 V | 200–240 mAh | No | BLE beacons, tiny sensors |
python
# Voltage monitoring to detect low battery
from machine import ADC, Pin
# Use a resistor divider to measure battery voltage
# If battery is 3.7V max and ADC range is 0-3.3V:
# Use 100kΩ + 100kΩ divider → 1.85V max into ADC
DIVIDER_RATIO = 2.0 # Adjust based on your resistor values
VREF = 3.3 # ESP32 ADC reference
batt_adc = ADC(Pin(35))
batt_adc.atten(ADC.ATTN_11DB)
def read_battery_voltage():
raw = batt_adc.read()
voltage = raw / 4095 * VREF * DIVIDER_RATIO
return round(voltage, 2)
def battery_percent(voltage, v_min=3.3, v_max=4.2):
"""Rough SoC estimate for Li-ion (non-linear in reality)."""
pct = (voltage - v_min) / (v_max - v_min) * 100
return max(0, min(100, round(pct)))
v = read_battery_voltage()
pct = battery_percent(v)
print(f"Battery: {v} V ({pct}%)")
if pct < 10:
print("LOW BATTERY — sending alert!")Solar Charging
Solar Panel → Charge Controller → Battery → Power Regulator → MCU
Typical setup for outdoor sensor node:
├── 5 V / 1 W solar panel (small, ~65 × 65 mm)
├── TP4056 LiPo charge controller module
├── 3.7 V / 2000 mAh 18650 battery
└── AMS1117-3.3 or ME6211 LDO regulator → ESP32
Energy budget (daily):
Solar generation (5h sun × 1W panel × 80% efficiency): 4 Wh
ESP32 consumption (5s awake @ 240mA + 295s sleep @ 10µA):
= 5 × 0.240 + 295 × 0.000010 = 1.2 + 0.003 = 1.203 Wh
Net positive: system self-sustains with surplus energy.Practical Power Optimization Tips
python
# Tip 1: Turn off WiFi when not needed
import network
wlan = network.WLAN(network.STA_IF)
# ... do your WiFi work ...
wlan.disconnect()
wlan.active(False) # Power down WiFi radio (~170 mA saved)
# Tip 2: Lower CPU frequency when doing simple tasks
import machine
machine.freq(80_000_000) # 80 MHz (vs default 240 MHz)
# Use 80 MHz for ADC reading, 240 MHz only when doing WiFi/BT
# Tip 3: Batch your data — one WiFi connection for multiple readings
buffer = []
for _ in range(10):
sensor.measure()
buffer.append({
't': sensor.temperature(),
'h': sensor.humidity()
})
machine.deepsleep(60_000) # Sleep 1 min between readings
# Connect once and send all buffered data at once
if len(buffer) == 10:
send_batch(buffer)
buffer.clear()
# Tip 4: Use RTC memory to persist small state across deep sleeps
import esp32
rtc_mem = esp32.NVS('myapp') # Non-volatile storage in RTC domain
rtc_mem.set_i32('count', rtc_mem.get_i32('count') + 1)Summary
- A ESP32 in deep sleep draws only ~10 µA vs 240 mA when active — a 24,000× difference
- Deep sleep clears RAM but is most power-efficient; light sleep preserves RAM
- Use a timer wakeup for periodic sensing and ext0/ext1 wakeup for event-driven operation
- 18650 Li-ion batteries + TP4056 charger is the most common IoT power setup
- For outdoor, unattended devices use solar + battery with a proper energy budget calculation
- Batch your WiFi connections — connect once, send all buffered readings, then sleep
Next, we'll explore the networking protocols that connect your IoT devices to the cloud.