E-Ink House-Status Panel — Project Plan & Build Guide¶
A wall-mounted, paper-like status board that shows the whole house at a glance: TOU electricity price, garage doors, indoor climate & air quality, garage food temps, and plant moisture. Built from an ESP32 + e-paper display running ESPHome, it pulls live entities from Home Assistant, holds the image with zero power between updates, and runs for weeks on a battery.
Status: Planned
Design complete, ready to build. Sibling DIY project to the Food Temperature Sensor Build.
Why build it¶
- Glanceable, calm, always-on. No glow, no backlight, no fan — e-ink looks like a printed page and only changes when something changes.
- The TOU headline is the killer feature. "Should I run the dryer right now?" answered from across the room, driven by your existing
sensor.tou_period/sensor.tou_rate. - Local & cheap. Pure ESPHome → Home Assistant; no cloud, ~$45–80 in parts.
- Surfaces work you've already done — garage state on the way out the door, the food-temp and fiddle-leaf-moisture tiles, air quality, weather.
Phase 0 — Decisions¶
Lock these before ordering. Defaults are the recommended pick.
| Decision | Options | Default & why |
|---|---|---|
| Panel size | 4.2" / 5.65" / 7.5" (800×480) | 7.5" — room for all the tiles while staying readable on a wall. |
| Color | Mono / 3-color (B/W/Red) / 7-color | 3-color — adds a red peak/alert banner (TOU peak, freezer failing) for almost no cost; refresh still fast enough. Mono is crisper/cheaper if you don't want red. |
| Power | Battery (LiPo) + deep sleep / USB | Battery if it hangs away from an outlet (weeks per charge). USB if there's a nearby outlet and you want faster updates. |
| Location | — | Pick the wall first — it drives cable/outlet and update cadence. |
This guide assumes the defaults
The reference config below is written for a 7.5" 3-color Waveshare panel on battery. Switching to mono = change the display model: and drop the red color:; switching to USB = remove the deep_sleep block and set a normal update_interval.
Phase 1 — Bill of materials¶
| Part | Qty | ~Cost | Notes |
|---|---|---|---|
| Waveshare 7.5" 3-color e-paper (B/W/Red) + ESP32 driver board | 1 | $40–55 | Best-supported panel in ESPHome (waveshare_epaper). The "e-Paper ESP32 Driver Board" combo is the easy buy. |
| (Alt all-in-one) LilyGo T5 4.7" or Inkplate 6 | 1 | $50–90 | Integrated ESP32 + screen + battery connector. Inkplate is tidier but more Arduino-native. |
| LiPo battery 2000–3000 mAh + JST | 1 | $8–12 | Skip if going USB. |
| Momentary push button | 1 | ¢ | Manual "refresh now" wake. |
| Frame / shadow box / 3D-printed bezel | 1 | $5–20 | A cheap deep photo frame works great. |
| USB power supply + cable | 1 | $5 | For flashing (and for the USB-powered option). |
Total ≈ $45–80.
Phase 2 — Bring-up (hardware + "hello world")¶
Goal: ESP32 boots ESPHome and draws something on the e-ink.
- Seat the panel on the driver board (or assemble the all-in-one).
- In Home Assistant → ESPHome → New Device, create
house-status-panel. - Flash a minimal config that just prints text + the time (see the skeleton in the reference config). USB-flash the first time; OTA after.
Done when: the panel shows the date/time and refreshes on a timer.
Phase 3 — Pull Home Assistant data¶
Goal: Live entity values appear on screen.
Import each entity with the homeassistant platform (these fetch over the API connection — no templates needed), then reference them in the display lambda. The entities to wire:
| Tile | Entities |
|---|---|
| TOU | sensor.tou_period, sensor.tou_rate |
| Weather | weather.forecast_home (state + temperature attribute) |
| Garage | cover.left_garage_door, cover.right_garage_door (verify your IDs) |
| Climate / AQ | sensor.bedroom_temperature, sensor.office_temperature, sensor.bedroom_carbon_dioxide |
| Food | sensor.refrigerator_temperature_sensor_temperature, sensor.freezer_temperature_sensor_temperature, sensor.refrigerator_freezer_temperature_sensor_temperature |
| Plants | sensor.fiddle_leaf_1_moisture, sensor.fiddle_leaf_2_moisture, sensor.soil_sensor_1_moisture |
Done when: real numbers render (even as plain rows of text).
Phase 4 — Final layout & icons¶
Goal: It looks like a dashboard, not a debug log.
- Add fonts (a clean sans like Roboto at a few sizes) and the Material Design Icons font for glyphs (garage, weather, plant, snowflake).
- Lay out the tiles to match this mock:
┌─────────────────────────────────────────────┐
│ BLAIRMONT Thu Jun 11 2:41 PM │
├────────────────┬──────────────────────────────┤
│ ⚡ OFF-PEAK │ ☀ 84° ↑88 ↓67 │
│ $0.07 / kWh │ Partly cloudy │
├────────────────┴──────────────────────────────┤
│ 🚪 Garage L: Closed R: Closed │
│ 🌡 Bed 71° Office 73° CO₂ 612 ppm │
├───────────────────────────────────────────────┤
│ ❄ FOOD Fridge 39° Freezer −2° ✓ │
│ 🪴 Fiddle1 18% ⚠ Fiddle2 59% Maury 52% │
└───────────────────────────────────────────────┘
updated 2:41 PM · next 2:56 PM
- Use the red color for the TOU banner only when
tou_period == "peak", and for a freezer/fridge "FAILING" state — so red always means "pay attention."
Done when: the layout is legible across the room and red appears only on alerts.
Phase 5 — Power & deep sleep¶
Goal: Weeks of battery life with a manual refresh option.
- Add the
deep_sleepcomponent: wake on a timer (e.g. every 15 min), connect, let HA push states, redraw, sleep. - Wire the push button to an RTC-capable GPIO as a
wakeup_pinfor "refresh now."
Generic dev boards leak power
Cheap ESP32 dev boards waste several mA in deep sleep (onboard USB-serial chip + power LED), which guts battery life. For real longevity use a low-power board (LilyGo, FireBeetle) or one with the USB chip/LED removed. Expect weeks on a 2500 mAh cell at 15-min wakes on a good board; months if you slow the cadence.
Done when: it sleeps between updates and the button forces an immediate refresh.
Phase 6 — Mount¶
- Seat the panel in the frame/bezel, route the battery and (optional) USB.
- Hang it. Done.
Refresh strategy¶
- Full refresh (the black/white flash) every wake is fine for a status board and prevents ghosting on the 3-color panel; color panels can't do partial refresh anyway.
- E-ink is not real-time — it's perfect for things that change over minutes (TOU period, garage, temps), useless for live graphs. That's exactly this use case.
- A color/3-color full refresh takes ~15–30 s; the deep-sleep
run_durationmust comfortably exceed that so the redraw completes before sleep.
Reference: ESPHome config¶
Starting point, not gospel
Layout coordinates are a first pass — expect to nudge x/y values. Add Material Design Icon glyphs by their unicode codepoints for each icon you use.
substitutions:
name: house-status-panel
esphome:
name: ${name}
friendly_name: House Status Panel
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
api:
encryption:
key: !secret house_panel_api_key
on_client_connected: # deep-sleep loop: redraw once HA is connected, then sleep
- delay: 4s # let HA push entity states
- component.update: eink
- delay: 30s # let the (slow) 3-color refresh finish
- deep_sleep.enter: ds
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
time:
- platform: homeassistant
id: ha_time
deep_sleep:
id: ds
sleep_duration: 15min
run_duration: 90s # safety cap; on_client_connected normally sleeps sooner
wakeup_pin: # manual "refresh now" button
number: GPIO39
inverted: true
# --- HA entity imports ---
sensor:
- platform: homeassistant
id: tou_rate
entity_id: sensor.tou_rate
- platform: homeassistant
id: out_temp
entity_id: weather.forecast_home
attribute: temperature
- platform: homeassistant
id: bed_temp
entity_id: sensor.bedroom_temperature
- platform: homeassistant
id: office_temp
entity_id: sensor.office_temperature
- platform: homeassistant
id: bed_co2
entity_id: sensor.bedroom_carbon_dioxide
- platform: homeassistant
id: fridge_t
entity_id: sensor.refrigerator_temperature_sensor_temperature
- platform: homeassistant
id: freezer_t
entity_id: sensor.freezer_temperature_sensor_temperature
- platform: homeassistant
id: fiddle1
entity_id: sensor.fiddle_leaf_1_moisture
- platform: homeassistant
id: fiddle2
entity_id: sensor.fiddle_leaf_2_moisture
- platform: homeassistant
id: maury
entity_id: sensor.soil_sensor_1_moisture
text_sensor:
- platform: homeassistant
id: tou_period
entity_id: sensor.tou_period
- platform: homeassistant
id: garage_l
entity_id: cover.left_garage_door
- platform: homeassistant
id: garage_r
entity_id: cover.right_garage_door
font:
- file: "gfonts://Roboto"
id: f_big
size: 40
- file: "gfonts://Roboto"
id: f_med
size: 24
- file: "gfonts://Roboto"
id: f_small
size: 18
- file: "https://github.com/Templarian/MaterialDesign-Webfont/raw/master/fonts/materialdesignicons-webfont.ttf"
id: f_icons
size: 30
glyphs: ["\U000F0D5A", "\U000F0E1B", "\U000F050F", "\U000F1A1B"] # add codepoints per icon used
color:
- id: red
red: 100%
green: 0%
blue: 0%
display:
- platform: waveshare_epaper
model: 7.50inV2 # use the 3-color model id for your exact panel
update_interval: never # only redraw when we call component.update
id: eink
lambda: |-
// Header
it.strftime(10, 8, id(f_med), "%a %b %d %-I:%M %p", id(ha_time).now());
it.print(10, 8, id(f_med), "BLAIRMONT");
// TOU banner — red only during peak
bool peak = id(tou_period).state == "peak";
auto c = peak ? id(red) : COLOR_ON;
it.printf(10, 60, id(f_big), c, "%s", peak ? "ON PEAK" :
(id(tou_period).state == "off_peak" ? "OFF-PEAK" : "SUPER OFF"));
it.printf(10, 108, id(f_med), c, "$%.2f / kWh", id(tou_rate).state);
// Weather (right column)
it.printf(420, 60, id(f_big), "%.0f°", id(out_temp).state);
// Garage
it.printf(10, 170, id(f_med), "Garage L: %s R: %s",
id(garage_l).state.c_str(), id(garage_r).state.c_str());
// Climate / AQ
it.printf(10, 210, id(f_med), "Bed %.0f° Office %.0f° CO2 %.0f",
id(bed_temp).state, id(office_temp).state, id(bed_co2).state);
// Food — red if a freezer is failing
bool food_bad = id(freezer_t).state > 20 || id(fridge_t).state > 50;
it.printf(10, 270, id(f_med), food_bad ? id(red) : COLOR_ON,
"FOOD Fridge %.0f° Freezer %.0f°", id(fridge_t).state, id(freezer_t).state);
// Plants — red if a fiddle leaf is dry
it.printf(10, 310, id(f_med), id(fiddle1).state < 30 ? id(red) : COLOR_ON,
"Fiddle1 %.0f%% Fiddle2 %.0f%% Maury %.0f%%",
id(fiddle1).state, id(fiddle2).state, id(maury).state);
// Footer
it.strftime(10, 440, id(f_small), "updated %-I:%M %p", id(ha_time).now());
Secrets
house_panel_api_key, ota_password, wifi_ssid, wifi_password live in ESPHome's own secrets.yaml.
Troubleshooting¶
| Symptom | Fix |
|---|---|
| Screen blank / never updates | Confirm the display model: matches your exact panel; check the SPI wiring on the driver board. |
Values show as nan / blank | The homeassistant sensor's entity_id is wrong, or HA hasn't pushed state yet — increase the on_client_connected delay. |
| Device sleeps before redraw finishes | Increase the post-update delay and run_duration (3-color refresh is slow). |
| Battery dies in days | Generic dev board leaking in deep sleep — switch to a low-power board or remove the USB chip/LED. |
| Ghost image / faint old text | Force a periodic full refresh (the flashing kind); avoid partial refresh on 3-color. |
Cost & effort¶
- ≈ $45–80, an evening or two — mostly polishing the layout lambda. The HA wiring and deep-sleep loop are boilerplate.
Related: Food Temperature Sensor Build · Energy & Power · Climate & Comfort · Grafana Analytics