Battery-Powered ESP32 Weather Display with Deep Sleep
Fetch current weather from OpenWeatherMap API and display it on a 2.9" e-paper screen. ESP32 WROOM enters deep sleep between updates for 30+ day battery life on a single 18650 cell.
Power Budget: Why 30+ Days Is Achievable
In deep sleep the ESP32 draws around 10 µA. Waking up, connecting to WiFi, fetching the API response, updating the e-paper display, and going back to sleep takes about 8 seconds and draws roughly 80 mA average.
With a 30-minute update interval and a 3000 mAh 18650 cell:
- Active: (8s / 1800s) × 80 mA = 0.36 mA average during active
- Sleep: 1792s × 0.01 mA = 17.92 mA-s ÷ 1800s = 0.010 mA
- Total ≈ 0.37 mA average → 3000 ÷ 0.37 ≈ 8100 hours ≈ 337 days
In practice WiFi reconnect variance, display refresh, and LiPo self-discharge reduce this to 30–60 days — still outstanding.
💡 Tip
E-paper displays retain the last image with zero power when not refreshing. Choose a 2.9" Waveshare module that supports **partial refresh** — partial refresh takes 0.3s vs 2s for full refresh and drastically reduces the active-time current draw.
Components required
// ── ESP32 Deep Sleep E-Paper Weather Display ─────────────────────────────────
// Libraries: GxEPD2, ArduinoJson, HTTPClient
// OpenWeatherMap API key required (free tier: 60 calls/min)
// ─────────────────────────────────────────────────────────────────────────────
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
// ── Config ────────────────────────────────────────────────────────────────────
#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASS "YOUR_PASS"
#define OWM_API_KEY "your_openweathermap_api_key"
#define CITY_ID "1261481" // Delhi — find yours at openweathermap.org/city
#define SLEEP_MIN 30 // Deep sleep interval in minutes
// ── E-Paper pins (Waveshare 2.9" on ESP32) ────────────────────────────────────
#define EPD_CS 5
#define EPD_DC 17
#define EPD_RST 16
#define EPD_BUSY 4
GxEPD2_BW<GxEPD2_290_T5D, GxEPD2_290_T5D::HEIGHT> epd(
GxEPD2_290_T5D(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));
// ── RTC memory — persists across deep sleep ───────────────────────────────────
RTC_DATA_ATTR int bootCount = 0;
struct WeatherData {
char city[32];
float tempC;
float feelsLike;
int humidity;
float windKmh;
char description[64];
char icon[8];
};
bool fetchWeather(WeatherData& w) {
String url = "http://api.openweathermap.org/data/2.5/weather?id=" +
String(CITY_ID) + "&appid=" + OWM_API_KEY + "&units=metric";
HTTPClient http;
http.begin(url);
int code = http.GET();
if (code != 200) { http.end(); return false; }
StaticJsonDocument<2048> doc;
if (deserializeJson(doc, http.getString())) { http.end(); return false; }
http.end();
strlcpy(w.city, doc["name"] | "Unknown", sizeof(w.city));
strlcpy(w.description, doc["weather"][0]["description"] | "",sizeof(w.description));
strlcpy(w.icon, doc["weather"][0]["icon"] | "", sizeof(w.icon));
w.tempC = doc["main"]["temp"] | 0.0f;
w.feelsLike= doc["main"]["feels_like"] | 0.0f;
w.humidity = doc["main"]["humidity"] | 0;
w.windKmh = (doc["wind"]["speed"] | 0.0f) * 3.6f; // m/s → km/h
return true;
}
void drawWeather(const WeatherData& w) {
epd.setRotation(1); // Landscape
epd.setFullWindow();
epd.firstPage();
do {
epd.fillScreen(GxEPD_WHITE);
epd.setTextColor(GxEPD_BLACK);
// City name
epd.setFont(&FreeMonoBold9pt7b);
epd.setCursor(4, 16);
epd.print(w.city);
// Temperature — big
epd.setFont(&FreeSansBold18pt7b);
epd.setCursor(4, 60);
epd.printf("%.1f", w.tempC);
epd.setFont(&FreeMonoBold9pt7b);
epd.print(" C");
// Details
epd.setFont(&FreeMonoBold9pt7b);
epd.setCursor(4, 82);
epd.printf("Feels: %.1fC Hum: %d%%", w.feelsLike, w.humidity);
epd.setCursor(4, 98);
epd.printf("Wind: %.1f km/h", w.windKmh);
epd.setCursor(4, 114);
epd.print(w.description);
// Boot count & next update
epd.setCursor(180, 128);
epd.printf("#%d | +%dmin", bootCount, SLEEP_MIN);
} while (epd.nextPage());
}
void setup() {
bootCount++;
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500); attempts++;
}
WeatherData weather;
if (WiFi.isConnected() && fetchWeather(weather)) {
epd.init(115200);
drawWeather(weather);
epd.hibernate(); // Cut e-paper power — image stays on screen
}
WiFi.disconnect(true);
esp_sleep_enable_timer_wakeup((uint64_t)SLEEP_MIN * 60 * 1000000ULL);
esp_deep_sleep_start();
// Execution never reaches here — ESP32 hard-resets on wake
}
void loop() {}Steps
- 1Get a free OpenWeatherMap API key at openweathermap.org — standard tier is free
- 2Find your city ID on openweathermap.org/find and set CITY_ID
- 3Install GxEPD2 library and match the constructor to your exact e-paper model
- 4Upload via USB on first boot — subsequent updates can be done over OTA if desired
- 5After first successful update, the screen shows weather and the ESP32 goes to sleep
- 6Solder to a TP4056 charger + MT3608 boost converter for a self-contained unit
- 7Optional: add a physical button wired to GPIO0 to force an immediate refresh