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
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 generationrequests- API callsshelve- Persistent cachingpython-dotenv- API key management
Client Side (ESP32/Arduino):
GxEPD2- E-ink display driverWiFi- 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 first test
What the Server Does
The Python server (eink_dashboard.py) runs on my Debian box and handles everything:
- Fetches data from weather, transport, and river APIs
- Caches intelligently using
shelveto avoid hammering APIs - Renders the dashboard with PIL/Pillow on an 800x480 canvas
- Converts to 1-bit BMP using Floyd-Steinberg dithering for smooth gradients
- 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:
- Wake - Connect to WiFi
- Download - Fetch
image.bmpvia HTTP - Display - Parse 1-bit BMP and render to e-ink
- 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)