AdvancedESP32ESP32Deep SleepE-Paper

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.

Circuit Hub40 min read1 views
Source code

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 WROOM-32 Dev Board
×1Buy
Waveshare 2.9" E-Paper Display (SPI)
×1Buy
18650 Li-ion Cell (3000mAh)
×1
TP4056 LiPo Charger Module (USB-C)
×1
MT3608 Boost Converter 5V output
×1
100 µF Capacitor (buffer for WiFi spike)
×1
Custom PCB or protoboard
×1
C++
// ── 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

  1. 1Get a free OpenWeatherMap API key at openweathermap.org — standard tier is free
  2. 2Find your city ID on openweathermap.org/find and set CITY_ID
  3. 3Install GxEPD2 library and match the constructor to your exact e-paper model
  4. 4Upload via USB on first boot — subsequent updates can be done over OTA if desired
  5. 5After first successful update, the screen shows weather and the ESP32 goes to sleep
  6. 6Solder to a TP4056 charger + MT3608 boost converter for a self-contained unit
  7. 7Optional: add a physical button wired to GPIO0 to force an immediate refresh

Related projects