Skip to content

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.

  1. Seat the panel on the driver board (or assemble the all-in-one).
  2. In Home Assistant → ESPHome → New Device, create house-status-panel.
  3. 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_sleep component: 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_pin for "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_duration must 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