Skip to main content
Building Zettelbox 2.0 – An ESPHome E-Ink Display Upgrade

Building Zettelbox 2.0 – An ESPHome E-Ink Display Upgrade

· 6 min read · < 100 views

Two years ago, I built a wooden notepad box with a 2.9-inch E-Ink display and wrote about it. Until recently, it was still sitting on my desk and it’s still working. It was even shared on Hacker News back then.

But there were two things that kept bothering me a bit. Not a huge deal, but still. That’s why I’ve been toying with the idea of an upgrade for a few months now. The display was too small I could just about fit the data I wanted on it, but that was about it. And the box itself looked exactly as it was: sawn by hand, filed by hand, and given a quick coat of paint. Quite obviously a DIY project.

The new display

#

The original Waveshare 2.9" panel had 296×128 pixels, landscape. The layout was always a compromise. Everything squeezed to fit.

The new panel is a Waveshare 2.7". Slightly smaller on the diagonal, but 264x176 pixels in landscape with a 3:2 ratio. More vertical space. The card-based layout I’d been sketching for a while actually worked on the first try.

The new box

#

Version 1 used 8 mm plywood for the front and 4 mm plywood for the rest. Everything was cut with a jigsaw and then filed down and sanded by hand.

For Zettlebox 2.0, my friend Eric helped me out. He has a hobby CNC milling machine. Using this, we cut all the parts from 4 mm plywood. This is half the thickness, which only works because the cuts are precise. The parts just fit. There was no need for filing afterwards, and only a little sanding was required for post-processing.

Eric also came up with the idea for the new curved front design. It’s a perfectly mirrored curved line that would be hard to make manually. But with Eric’s machine, it’s just an extra 5-minute step in the cutter program.

Zettelbox 2.0 cutting modelZettelbox 2.0 cutting model

This is the difference between something that was “made by hand over the weekend” and something that was “built properly”. The whole box looks more professional and intentional.

Bill of materials

#

Nothing exotic, it’s the same parts as the first version. I already had the display and driver board lying around.

I was so impressed by the CNC process, from the initial preparation in the cutter software right through to the final cutting, that I took a lot of pictures.

Zettelbox 2.0 cutting processZettelbox 2.0 cutting processZettelbox 2.0 cutting processZettelbox 2.0 cutting processZettelbox 2.0 cutting processZettelbox 2.0 cutting processZettelbox 2.0 cutting processZettelbox 2.0 cutting process

Software

#

It’s the same stack as v1: ESPHome and Home Assistant for almost all the data. There are now nine display pages instead of four, cycling every 30 seconds. The larger display allows me to display more sensor data and offers more design options.

Hardware setup

#

The 2.70" Waveshare wires up over SPI, same as the original. The main things to get right are the model string and the update strategy.

spi:
  clk_pin: GPIO13
  mosi_pin: GPIO14

display:
  - platform: waveshare_epaper
    id: epaper_display
    cs_pin: GPIO15
    dc_pin: GPIO27
    busy_pin: GPIO25
    reset_pin: GPIO26
    model: 2.70in
    rotation: 90
    update_interval: never

update_interval: never is intentional. E-ink flickers on every full refresh. Instead I trigger page changes on a 30-second interval and call component.update explicitly:

time:
  - platform: homeassistant
    id: homeassistant_time
    on_time:
      - seconds: /30
        then:
          - display.page.show_next: epaper_display
          - component.update: epaper_display

Sensors

#

Almost all sensor data still comes from Home Assistant. There’s just more of it in the full YAML file. A few examples are given below:

sensor:
  - platform: homeassistant
    entity_id: sensor.solaredge_energy_today
    id: energy_today

  - platform: homeassistant
    entity_id: sensor.polestar_4_battery_charge_level
    id: car_charge_level
    unit_of_measurement: "%"

  - platform: homeassistant
    entity_id: weather.jama_villa
    id: weather_temp
    attribute: temperature

The weather entity is worth calling out. One HA entity, but multiple attributes - condition, temperature, wind speed, bearing. Each attribute needs its own sensor block with the attribute: key.

Display pages

#

The display cycles through 9 pages:

  • Wetter — weather condition icon, temperature, wind speed and direction
  • Energie — solar yield, battery level, consumption, grid export/import, net balance
  • Pool — pump state, solar valve, water and heater temperatures
  • Klima — garden and indoor temperature, humidity, PM2.5 fine particulate
  • Auto — Polestar charge level, range, odometer, charging status
  • Abfall — next waste collection date
  • Blog — visitor and page view counts for markus-haack.com
  • Claude — session and weekly API usage with countdown to reset
  • System — WiFi signal, IP address, uptime, time
Zettelbox 2.0 - Auto pageZettelbox 2.0 - Wetter pageZettelbox 2.0 - Pool pageZettelbox 2.0 - Klima page

Each page is a lambda that draws directly onto the e-ink canvas with x/y coordinates. To keep all 9 consistent, I reuse a draw_card helper: rectangle, label at the top, value in large font at the bottom, unit beside it in small font.

auto draw_card = [&](int x, int y, const char* label,
                     const char* value, const char* unit) {
  int mid = x + CARD_W / 2;
  int val_base = y + CARD_H - 20;
  it.rectangle(x, y, CARD_W, CARD_H);
  it.print(x + PAD, y + PAD + 10, id(font_14),
           TextAlign::BASELINE_LEFT, label);
  it.print(mid, val_base, id(font_20),
           TextAlign::BASELINE_RIGHT, value);
  it.print(mid + 3, val_base, id(font_12),
           TextAlign::BASELINE_LEFT, unit);
};

Most of the data pages are two-column card grids, built using that helper. The energy page has three columns, while the weather page has a large, centred icon instead of cards. The same coordinate system is used everywhere, so it is easy to adjust for a different display later.

The page that wasn’t included in v1 is the blog stats page. Rather than routing visitor counts through Home Assistant, the ESP32 fetches them directly from Pirsch Analytics via HTTP on boot and then every 30 minutes. This is not a typical ESPHome pattern, but the ‘http_request’ component handles it well.

There is also a Claude API usage page now, showing the session and weekly quota with a countdown to each reset. I check it more often than I’d like to admit.

The full config is on GitHub.

Wrapping up

#

It’s the same idea as two years ago. It’s a small box on the desk that shows me what’s going on at home without me having to reach for my phone. This time it’s even better: a sharper display, a cleaner build, more data and a couple of extra pages that I created just for fun.

And, honestly, I’m happy with it. The display size is perfect, and the woodwork is exactly what I wanted — no improvements needed.

Or maybe one. It would actually be nice to have a colour display.