I built a wall-mounted e-ink dashboard that shows me everything I need to know before leaving the house: weather, train times, tram schedules, and even birthday reminders. It updates every 10 minutes and runs on battery for 3-4 months.

Older photo of e-ink display

Older photo of e-ink display

E-Ink dashboard screenshot of current layout

E-Ink dashboard screenshot of current layout

What I Used

Hardware

  • Waveshare 7.5" E-Ink Display (800x480, black & white)
  • ESP32 (for WiFi and controlling the display)
  • Linux Server (Debian box running Python)
  • 3D Printed Case (custom design for wall mounting)

Software Stack

Server Side (Python):

  • PIL/Pillow - Image generation
  • requests - API calls
  • shelve - Persistent caching
  • python-dotenv - API key management

Client Side (ESP32/Arduino):

  • GxEPD2 - E-ink display driver
  • WiFi - Network connectivity
  • Standard Arduino HTTP client

APIs:

  • OpenWeatherMap - Weather forecasts
  • Swiss Transport API - Trains and trams
  • Google Distance Matrix - Travel times
  • Basel Open Data - Rhine river data

System Architecture

The architecture is dead simple: a Python server does all the heavy lifting, and the ESP32 just fetches and displays the image.

┌─────────────┐
│   Weather   │
│  Transport  │──┐
│   Google    │  │
│  Rhine API  │  │
└─────────────┘  │
                 ↓
         ┌───────────────┐
         │ Python Server │
         │ - Fetch APIs  │
         │ - Cache data  │
         │ - Draw image  │
         │ - Convert BMP │
         └───────┬───────┘
                 │
          image.bmp (800x480, 1-bit)
                 │
                 ↓ HTTP
         ┌───────────────┐
         │     ESP32     │
         │ - Wake up     │
         │ - Download    │
         │ - Display     │
         │ - Sleep 10min │
         └───────┬───────┘
                 │
                 ↓
         ┌───────────────┐
         │ E-Ink Display │
         │   800x480     │
         └───────────────┘

Why this architecture?

  • ESP32 code stays simple - just fetch and display
  • Can update the dashboard layout without reflashing the ESP32
  • Python is way better at API calls and image manipulation
  • Minimal processing on the ESP32 = better battery life
E-Ink Display Screenshot

E-Ink Display first test

What the Server Does

The Python server (eink_dashboard.py) runs on my Debian box and handles everything:

  1. Fetches data from weather, transport, and river APIs
  2. Caches intelligently using shelve to avoid hammering APIs
  3. Renders the dashboard with PIL/Pillow on an 800x480 canvas
  4. Converts to 1-bit BMP using Floyd-Steinberg dithering for smooth gradients
  5. Serves via HTTP on port 80 for the ESP32 to download

Intelligent Caching

Different APIs have different cache times:

WEATHER_API_BUFFER_MINUTES = 10
TRANSPORT_API_BUFFER_MINUTES = 10
GOOGLE_MAPS_API_BUFFER_MINUTES = 18  # Expensive, cache longer
RHEIN_API_BUFFER_MINUTES = 10

The cache is persistent using Python's shelve module, so it survives restarts. Fresh cache skips API calls; stale cache triggers a refresh.

Dashboard Layout

  • Header: Date, day, wedding countdown, birthday notifications
  • Weather: Current conditions, temperature, UV index, Rhine data, 4-day forecast
  • Travel: ETAs to work and family via Google Maps
  • Transport: Train and tram departures
  • Footer: Last updated timestamp and version

What the Display Does

The ESP32 firmware is dead simple:

  1. Wake - Connect to WiFi
  2. Download - Fetch image.bmp via HTTP
  3. Display - Parse 1-bit BMP and render to e-ink
  4. Sleep - Deep sleep for 10 minutes, repeat

That's it.

API Calls

Swiss Transport API

The Swiss transport API is free and amazing. I fetch train and tram times:

# Get trains from Basel SBB
url = "http://transport.opendata.ch/v1/stationboard"
response = requests.get(url, params={
    'station': 'Basel SBB',
    'limit': 70
})

trains = response.json()['stationboard']

# Filter for only IR36 trains to Zurich
filtered = [t for t in trains if t['category'] == 'IR36']

Rhine River Data (Basel Open Data)

Basel has an awesome open data portal with real-time Rhine data:

# Temperature
temp_url = "https://data.bs.ch/api/v2/catalog/datasets/100046/records"
temp_response = requests.get(temp_url)
temperature = temp_response.json()['records'][0]['fields']['temperature']

# Water level
level_url = "https://data.bs.ch/api/v2/catalog/datasets/100089/records"
level_response = requests.get(level_url)
water_level = level_response.json()['records'][0]['fields']['pegel']

Weather API

OpenWeatherMap's One Call API 3.0 gives me everything in one request:

url = "https://api.openweathermap.org/data/3.0/onecall"
params = {
    'lat': 47.5596,  # Basel coordinates
    'lon': 7.5886,
    'units': 'metric',
    'appid': WEATHER_API_KEY
}

weather = requests.get(url, params=params).json()

current = weather['current']
daily = weather['daily'][:4]  # Next 4 days

Battery Life

Battery life is roughly 3-4 months using an 10,000mAh battery and an update cycle on the display every 10 minutes.

Power Optimization Ideas:

  • Increase sleep interval at night (e.g., 30 min from 11pm-6am)

Birthday Tracking

I keep a YAML file with birthdays:

# birthdays.yaml
- name: "John"
  date: "03-15"
- name: "Sarah"
  date: "07-22"

The dashboard checks every day and shows a cake icon with names if it's someone's birthday.

Next Steps

Things I want to add:

  • Battery voltage monitoring (show percentage on screen)
  • Calendar integration (Google Calendar events)
  • News headlines from RSS feeds

Conclusion

This project taught me a lot about e-ink displays, power management, and API optimization. The client-server architecture keeps things simple and flexible. I can update the dashboard layout anytime by just editing Python code on my server, no firmware reflashing needed.

Total cost: ~$60 (display + ESP32) excl. battery
Battery life: 3-4 months (10,000mAh battery)
Update frequency: Every 10 minutes
APIs used: 4 (Weather, Transport, Google Maps, Rhine)