PromptHub
Developer Tools Smart Home

Stop Wasting Power on LCD Screens! This E-Ink Dashboard Changes Everything

B

Bright Coding

Author

9 min read
47 views
Stop Wasting Power on LCD Screens! This E-Ink Dashboard Changes Everything

Stop Wasting Power on LCD Screens! This E-Ink Dashboard Changes Everything

Your smart home is bleeding electricity through the eyeballs. That gorgeous 27-inch monitor showing your calendar? It's devouring 30 watts. That always-on tablet displaying weather? Cooking itself to death in a sunny kitchen. Meanwhile, a quiet revolution is happening in bedrooms and hallways across the world—developers are ripping out power-hungry screens and replacing them with something that sips milliwatts, lasts months on battery, and looks impossibly cool doing it.

E-paper. The technology Kindles made famous. Now weaponized for your home.

But here's the brutal truth: building a custom e-ink dashboard used to require soldering skills, embedded C nightmares, and a PhD in frustration. You'd spend weekends wrestling with display controllers, bitmap formats, and WiFi stacks that randomly disconnect at 3 AM. Most developers gave up. Most should have given up.

Then Kyle Turman's tweet detonated across developer Twitter. A weather dashboard, rendered as HTML, converted to 1-bit PNG, served to a microcontroller-powered e-paper display that refreshes every 10 minutes and sleeps through the night. The response was instant and explosive. Thousands of developers saw what they'd been missing: a production-ready, modular, open-source home dashboard that bridges web technologies and embedded hardware without the usual pain.

That project is home-dashboard. And this guide will show you exactly how to build it, customize it, and make it yours.


What Is home-dashboard?

home-dashboard is a modular, open-source Node.js server designed to run on any always-on computer within your local network—Raspberry Pi, Mac Mini, old laptop, whatever's collecting dust in your closet. It aggregates data from multiple APIs (weather, calendar, vehicle telemetry, AI insights), renders everything as clean HTML/CSS, converts that to a 1-bit PNG image, and serves it over HTTP to a microcontroller-connected e-paper display.

The project exploded after Kyle Turman's viral tweet, and for good reason. It solves a genuinely hard problem: how do you create rich, dynamic information displays without the power consumption, heat, and visual fatigue of traditional screens?

E-paper displays reflect ambient light like real paper. No backlight. No glare. Readable in direct sunlight. And when they're not refreshing? They consume essentially zero power. The display in your dashboard can run for months on a small battery, waking briefly every 10 minutes to fetch a fresh image before returning to deep sleep.

What makes home-dashboard special isn't just the hardware integration—it's the architectural sanity. Kyle built this with web developers in mind. You're not writing C++ for the main logic. You're writing JavaScript. You're styling with CSS. You're templating with EJS. The embedded C++ code on the microcontroller is minimal, pre-written, and largely configuration-only. The hard stuff—API aggregation, image generation, error handling—is all Node.js, running where it belongs.

The project is production-stable (it powers Kyle's actual home display) but intentionally positioned as a starting point. The README candidly admits the code and CSS are "messier than I would like" and the 1-bit image conversion has font hinting issues. But as Kyle notes: "done is better than perfect and it's a home project so c'est la vie!" This honesty is refreshing. You're getting something that works, that you can build on, not a polished demo that falls apart when you touch it.


Key Features That Make This Insanely Powerful

Modular Service Architecture

Every data source extends BaseService, a foundation that provides automatic caching with configurable TTL, exponential backoff retry logic, stale cache fallback when APIs fail, and status tracking. This isn't naive polling—it's resilient, production-grade data fetching. When Visual Crossing hiccups, your display still shows something. When your WiFi flakes, the system recovers gracefully.

1-Bit PNG Pipeline

The secret sauce: HTML/CSS renders to a dashboard, then converts to a 1-bit (pure black and white) PNG. This isn't dithering for aesthetic reasons—e-paper displays are literally 1-bit devices. Each pixel is either black or white. The conversion pipeline respects this hardware reality, producing crisp (if occasionally hint-challenged) output that the microcontroller can blast directly to the display controller without transformation.

PM2 Process Management

The server runs as a persistent daemon via PM2, automatically restarting on crashes and optionally starting on boot. This matters because your dashboard needs to survive power outages, system updates, and the inevitable Node.js memory leak. PM2 handles the operational complexity so you don't become your own on-call engineer.

Multi-API Integration

  • Visual Crossing Weather: Multi-location forecasts, hourly data, astronomy (sunrise/set, moon phase). Free tier: 1,000 calls/day.
  • Ambient Weather Station: Real-time hyperlocal data if you own personal weather hardware.
  • Google Calendar: Upcoming events via OAuth 2.0.
  • Smartcar API: Vehicle battery/fuel level and range for supported cars.
  • Anthropic Claude: AI-generated daily insights and clothing suggestions for pennies per month.

Embedded Hardware Support

Tested on Seeed XIAO ESP32-C3, ESP32-S3, and reTerminal E1002 with 7.5" e-paper displays (800x480, UC8179 controller). Battery monitoring auto-enables on ESP32-S3. The Arduino sketch handles WiFi connection, HTTP image fetching, display refresh, and deep sleep scheduling.

Zero-Restart Development

Modify views/dashboard.ejs or CSS files and refresh your browser—no server restart needed. The e-paper image endpoint updates simultaneously. Rapid iteration on a platform that traditionally required flash-and-pray embedded development.


Use Cases Where This Absolutely Shines

1. The Bedroom Weather Station

Imagine waking to a paper-like display showing today's forecast, what to wear, your first calendar meeting, and whether your car needs charging—all without the sleep-disrupting glow of a backlight. The display sleeps from 12 AM to 5 AM by default. You get information when you need it, darkness when you don't.

2. The Kitchen Command Center

Recipes splatter on tablets. Phones die in pockets. A wall-mounted e-paper display shows your family's schedule, weather alerts, and grocery reminders without ever needing a charge cable. Mount it with 3M strips. Forget about it for months.

3. The Garage Vehicle Monitor

EV owners obsess over charge levels. Instead of opening an app, glancing at a wall-mounted e-paper display tells you range, charging status, and whether pre-conditioning is needed for tomorrow's commute. Smartcar integration makes this automatic.

4. The Off-Grid Cabin Display

Solar-powered remote locations can't afford 30-watt screens. A small solar panel + battery + ESP32 + e-paper creates a self-sustaining information display showing weather, moon phases for night navigation, and calendar reminders. The entire system draws under 100mW average.

5. The Office Availability Indicator

Mount outside a home office door. Calendar integration shows "In Meeting Until 3 PM" or "Available." No more interruptions, no more Slack status checking. Physical, glanceable, always correct.


Step-by-Step Installation & Setup Guide

Prerequisites

  • Node.js v16+ and npm
  • An always-on computer (Raspberry Pi 4 recommended, any Mac/PC/Linux works)
  • Optional: Seeed XIAO ESP32 + 7.5" e-paper display for hardware output

Step 1: Clone and Install

# Clone the repository
git clone https://github.com/kyleturman/home-dashboard.git
cd home-dashboard

# Install all dependencies
npm install

This installs Express for the web server, EJS for templating, image conversion libraries, PM2 for process management, and API client libraries.

Step 2: Configure Environment

# Copy the example environment file
cp .env.example .env

Edit .env with your settings. Minimum required:

# Your primary location (US ZIP code)
MAIN_LOCATION_ZIP=94607

# Get free API key from visualcrossing.com/weather-api
VISUAL_CROSSING_API_KEY=your_key_here

Optional but powerful additions:

# Additional weather locations (up to 3)
ADDITIONAL_LOCATION_ZIPS=90210,10001,60601

# Custom server port
PORT=7272

# Personal weather station (Ambient Weather)
AMBIENT_APPLICATION_KEY=your_app_key
AMBIENT_API_KEY=your_api_key

# Google Calendar OAuth
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
GOOGLE_REDIRECT_URI=http://localhost:7272/auth/google/callback

# Vehicle telemetry (Smartcar)
SMARTCAR_CLIENT_ID=your_client_id
SMARTCAR_CLIENT_SECRET=your_client_secret
SMARTCAR_REDIRECT_URI=http://localhost:7272/auth/smartcar/callback
SMARTCAR_MODE=live

# AI insights (Anthropic Claude)
ANTHROPIC_API_KEY=your_api_key

Step 3: Start the Server

# Start as PM2 daemon (auto-restarts on crash, survives terminal close)
npm start

# Useful commands
npm stop       # Stop the service
npm restart    # Restart and reload .env changes
npm run logs   # View live logs for debugging

The server binds to port 7272 by default. Verify it's running:

# Check dashboard in browser
curl http://localhost:7272/dashboard

# Check the e-paper image output
curl http://localhost:7272/dashboard/image -o test.png

Step 4: Enable Auto-Start on Boot (Critical!)

# Generate startup script for your OS
npx pm2 startup

# Run the command it outputs (usually requires sudo)
# Example: sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u pi --hp /home/pi

# Save current PM2 process list
npx pm2 save

This ensures your dashboard survives power outages and reboots without manual intervention.

Step 5: Hardware Setup (E-Paper Display)

Configure the Arduino sketch with your network details:

// In arduino/epaper-client/epaper-client.ino
const char* WIFI_SSID = "YourNetworkName";
const char* WIFI_PASSWORD = "YourPassword";
const char* SERVER_IP = "192.168.1.50";  // Your server's fixed IP
const int SERVER_PORT = 7272;

Critical: Assign your server a static IP in router settings or use mDNS hostname (raspberrypi.local). If the IP changes, your display becomes a very expensive paperweight until reconfigured.

Flash to ESP32 via Arduino IDE with ESP32 board support added. The display fetches http://YOUR_SERVER:7272/dashboard/image every 10 minutes, sleeps 12 AM–5 AM.


REAL Code Examples From the Repository

Let's dissect actual code from home-dashboard to understand how this system operates under the hood.

Example 1: The Core Data Pipeline

The dataBuilder.js aggregates all services into a unified dashboard object. Here's the architectural pattern:

// lib/dataBuilder.js - Simplified conceptual flow
const services = [
  new WeatherApiService(),      // Visual Crossing forecasts
  new AmbientService(),         // Personal weather station (optional)
  new CalendarService(),        // Google Calendar (optional)
  new VehicleService(),         // Smartcar telemetry (optional)
  new LLMService()              // Claude AI insights (optional)
];

// Each service extends BaseService with caching, retries, fallback
async function buildDashboardData() {
  const results = await Promise.allSettled(
    services.map(s => s.fetchWithResilience())
  );
  
  // Merge successful results, keep stale cache for failures
  return results.reduce((dashboard, result, index) => {
    if (result.status === 'fulfilled') {
      dashboard[services[index].key] = result.value;
    } else {
      dashboard[services[index].key] = services[index].getStaleCache();
    }
    return dashboard;
  }, {});
}

Why this matters: Promise.allSettled() ensures one failing API doesn't crash your entire dashboard. The getStaleCache() fallback means your display shows yesterday's weather rather than a blank screen. This resilience pattern is what separates toys from production systems.

Example 2: Service Architecture with BaseService

Every data source inherits from BaseService. Here's how you'd implement a custom service:

// lib/BaseService.js provides the foundation
class BaseService {
  constructor(options = {}) {
    this.cacheTTL = options.cacheTTL || 600000;  // 10 minutes default
    this.retryAttempts = options.retryAttempts || 3;
    this.status = 'unknown';
  }

  async fetchWithResilience() {
    // Check fresh cache first
    const cached = this.getCache();
    if (cached && !this.isStale(cached)) {
      this.status = 'cached';
      return cached.data;
    }

    // Attempt fetch with exponential backoff
    for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
      try {
        const data = await this.fetch();
        this.setCache(data);
        this.status = 'ok';
        return data;
      } catch (err) {
        await this.delay(Math.pow(2, attempt) * 1000);  // 2s, 4s, 8s
      }
    }

    // All retries failed - return stale cache or throw
    this.status = 'error';
    if (cached) return cached.data;  // Stale fallback
    throw new Error(`${this.constructor.name} failed after ${this.retryAttempts} attempts`);
  }
}

The power here: New services need only implement fetch(). Caching, retries, status tracking, and stale fallback come free. This is how you build systems that don't wake you at 3 AM.

Example 3: EJS Template for Dashboard Rendering

The HTML/CSS dashboard lives in views/dashboard.ejs. This is where you customize what appears on your display:

<!-- views/dashboard.ejs - Condensed example structure -->
<!DOCTYPE html>
<html>
<head>
  <style>
    /* Critical: designs must work in 1-bit (pure black/white) */
    body {
      width: 800px;      /* Match your display resolution */
      height: 480px;
      margin: 0;
      font-family: 'Inter', sans-serif;
    }
    .weather-main {
      font-size: 72px;
      font-weight: 700;
    }
    .weather-detail {
      font-size: 24px;
    }
    /* No gradients, no shadows, no anti-aliased gray */
  </style>
</head>
<body>
  <div class="grid">
    <section class="weather">
      <div class="weather-main"><%= weather.temp %>°</div>
      <div class="weather-detail"><%= weather.condition %></div>
      <% if (weather.hourly) { %>
        <div class="hourly">
          <% weather.hourly.slice(0, 6).forEach(h => { %>
            <span><%= h.time %>: <%= h.temp %>°</span>
          <% }) %>
        </div>
      <% } %>
    </section>
    
    <% if (calendar && calendar.events.length) { %>
      <section class="calendar">
        <h2>Today</h2>
        <% calendar.events.slice(0, 3).forEach(event => { %>
          <div class="event">
            <time><%= event.startTime %></time>
            <span><%= event.summary %></span>
          </div>
        <% }) %>
      </section>
    <% } %>
    
    <% if (llm && llm.insight) { %>
      <section class="ai-insight">
        <blockquote><%= llm.insight %></blockquote>
      </section>
    <% } %>
  </div>
</body>
</html>

Critical design constraint: Every visual element must survive conversion to 1-bit. No subtle grays, no gradients, no thin fonts that disappear. The 800x480 resolution matches the 7.5" display—design pixel-perfect for your exact hardware.

Example 4: Arduino Client - The Embedded Side

The microcontroller code is remarkably minimal. Here's the core loop from arduino/epaper-client/epaper-client.ino:

#include <WiFi.h>
#include <HTTPClient.h>
#include "driver.h"           // Display controller driver
#include "partial-refresh.h"  // Optimized refresh regions

// Configuration (set these!)
const char* WIFI_SSID = "MyHomeNetwork";
const char* WIFI_PASSWORD = "mypassword123";
const char* SERVER_IP = "192.168.1.50";
const int SERVER_PORT = 7272;
const unsigned long REFRESH_INTERVAL = 600000;  // 10 minutes in ms
const int SLEEP_START_HOUR = 0;   // 12 AM
const int SLEEP_END_HOUR = 5;     // 5 AM

void setup() {
  Serial.begin(115200);
  initDisplay();      // Initialize UC8179 controller
  connectWiFi();      // Block until connected
  
  // Fetch and display immediately on boot
  fetchAndDisplay();
}

void loop() {
  // Check if we're in sleep hours
  int currentHour = getCurrentHour();
  if (currentHour >= SLEEP_START_HOUR && currentHour < SLEEP_END_HOUR) {
    // Calculate sleep duration until 5 AM
    unsigned long sleepMs = ((5 - currentHour) * 3600000UL);
    deepSleep(sleepMs);  // Ultra-low power sleep
  }
  
  // Normal operation: fetch, display, sleep 10 minutes
  fetchAndDisplay();
  deepSleep(REFRESH_INTERVAL);
}

void fetchAndDisplay() {
  HTTPClient http;
  String url = String("http://") + SERVER_IP + ":" + SERVER_PORT + "/dashboard/image";
  
  http.begin(url);
  int httpCode = http.GET();
  
  if (httpCode == 200) {
    // Stream 1-bit PNG directly to display
    WiFiClient* stream = http.getStreamPtr();
    displayPngStream(stream);  // Driver handles UC8179 protocol
  } else {
    // Show error indicator (optional: retry logic here)
    displayErrorIcon();
  }
  
  http.end();
}

What's remarkable: The ESP32 spends almost its entire existence in deepSleep(). WiFi radio off. CPU halted. Display holding its image without power. A 2000mAh battery can theoretically last months with this duty cycle.


Advanced Usage & Best Practices

Optimize Image Quality

The 1-bit conversion has known font hinting issues. Mitigate by:

  • Using bold font weights (700+) exclusively
  • Increasing font sizes (24px minimum for readability)
  • Adding text-shadow-like effects with multiple offset copies
  • Testing via /dashboard/image endpoint before deploying to hardware

Service Testing During Development

# Test individual APIs without full server restart
npm run test-service weather   # Verify Visual Crossing connectivity
npm run test-service ambient   # Check personal station data
npm run test-service calendar  # Validate OAuth tokens
npm run test-service vehicle   # Confirm Smartcar pairing
npm run test-service llm       # Test Claude insight generation

Monitor Production Health

npx pm2 list      # See process status, uptime, restarts
npx pm2 monit     # Real-time CPU/memory dashboard
npx pm2 logs      # Stream logs with timestamps

Custom Vehicle Logos

Add brand PNGs to views/assets/vehicle-logo/ named lowercase with hyphens (audi.png, land-rover.png). Size: 40x40px minimum, single color. Falls back to text if no match.

Static IP Non-Negotiable

Your server IP will change on DHCP. Either:

  • Router reservation by MAC address
  • mDNS hostname (hostname.local)
  • Hardcoded IP with network infrastructure discipline

Comparison With Alternatives

Feature home-dashboard MagicMirror² Dakboard Custom ESP32+C++
Power Consumption ~0.1W average (e-paper) 15-30W (LCD) 10-20W (LCD/tablet) ~0.1W (but dev time: weeks)
Dev Experience Node.js, CSS, EJS Electron, complex module system No-code (limited) C++, no framework
E-Paper Native ✅ Designed for it ❌ LCD-focused ❌ LCD/tablet only ✅ But build everything
API Integrations Weather, Calendar, Car, AI, Personal Station 1000+ modules Paid tiers only Whatever you code
Offline Resilience ✅ Stale cache fallback ❌ Depends on module ❌ Cloud-dependent ✅ If you implement
Setup Complexity Medium High Low Very High
Cost Free (hardware ~$50-80) Free (hardware ~$100-300) $5-10/month subscription Free (hardware ~$30 + sanity)
Customization Full source, modular services Module-based, JS-heavy Limited to UI options Unlimited, at massive time cost

home-dashboard wins when: You want e-paper's power efficiency, need deep customization, and prefer web technologies over embedded C++ for application logic.


FAQ

Does this work with e-paper displays other than 7.5"?

The Arduino code targets 800x480 UC8179 controllers. Other sizes require modifying driver.h and potentially the image dimensions. The Node.js server itself is display-agnostic.

Can I run this without a Raspberry Pi?

Absolutely. Any always-on computer works: old laptop, Mac Mini, Intel NUC, even a Docker container on your NAS. The server just needs Node.js and network access.

How much does the weather API cost?

Visual Crossing's free tier provides 1,000 calls/day. At 6 calls/hour (10-minute refresh), you use ~144/day. Well within limits. Claude AI costs pennies monthly at Haiku pricing.

Is the display visible in the dark?

No—e-paper requires ambient light like physical paper. For nighttime visibility, add a motion-activated LED strip or position near existing lighting. The tradeoff is worth it for the power savings.

Can I add my own data sources?

Yes. Extend BaseService, implement fetch(), add to dataBuilder.js, update the EJS template. The modular architecture makes this straightforward. See AGENTS.md for AI-assisted development guidance.

What if my WiFi goes down?

The display retains its last image indefinitely without power. The server caches data and serves stale content during API outages. When connectivity returns, everything resynchronizes automatically.

How do I update the display layout?

Edit views/dashboard.ejs and CSS files, refresh browser. No server restart needed for template changes. The /dashboard/image endpoint reflects updates immediately.


Conclusion

The home-dashboard project represents something rare: a bridge between comfortable web development and the raw efficiency of embedded systems. You don't sacrifice your JavaScript skills. You don't learn a new framework. You take technologies you already know—Node.js, CSS, HTTP—and apply them to hardware that belongs in science fiction: a screen that displays information for months without charging, that looks like paper, that simply works.

Is the code perfect? No. Kyle will tell you that himself. But it works. It survived a viral tweetstorm. It's running in production homes right now. And it's yours to extend, customize, and make beautiful.

The alternative is another glowing rectangle, another device to charge, another source of blue light and distraction. Or you can build something different. Something that respects your attention, your electricity bill, and your craft.

Clone the repository. Flash an ESP32. Mount a display. Join the quiet revolution.

👉 Get home-dashboard on GitHub

MIT licensed. No warranty. Infinite possibility.

Comments (0)

Comments are moderated before appearing.

No comments yet. Be the first to share your thoughts!

Support us! ☕