A complete ESPHome firmware for the M5Stack PaperColor (SKU C151) — the 4″ Spectra-6 colour e-ink device — turning it into a battery-capable Home Assistant wall panel.
ESPHome doesn't support this device out of the box, so this repo provides the two missing pieces:
- A custom ESPHome external component (
m5pm1_power) for the on-board M5PM1 PMIC — without it the panel can't power its own e-ink rail, won't charge, and freezes the moment USB is unplugged. The component handles EPD-rail power-up, battery charging + power-hold (survives USB-unplug), battery voltage (median-filtered), USB/charge detection, and the power button (single press → "Off" screen → PMIC shutdown). - A working
epaper_spidisplay config for the Spectra-6 panel (mainline ESPHome'sSpectra-E6model — no fork needed) plus a practical server-rendered dashboard pattern.
Reverse-engineered from M5Stack's open-source M5GFX / M5PM1 register maps. Not affiliated with or endorsed by M5Stack. Use at your own risk — it drives a PMIC over I²C.
🤖 Built with AI assistance. This project was developed with Anthropic's Claude Code on real hardware — the PMIC reverse-engineering, the firmware/component, and the docs were AI-drafted and then verified on the device. It works on my unit, but treat it as a starting point and review the code before flashing your own.
The panel on a 3D-printed stand (printable .3mf included), showing the live overview page — note the on-device battery indicator top-right.
More example carousel pages (mock data) — the buttons page through them:
- Server-rendered dashboard — a Lovelace view is rendered to a 6-colour PNG by a renderer (e.g. the Puppet add-on) and blitted to the panel. Content is decoupled from the device: edit the dashboard in HA, no reflash.
- 5-page carousel with the 3 user buttons: refresh / previous / next (wraps).
- Adaptive power (PMIC full-shutdown on battery) — always-on while on USB; on battery it fully shuts down via the M5PM1 PMIC between refreshes (the e-ink image persists at zero power) and self-powers-on via the PMIC's own timer every 15 min, for ~1.5–2 weeks/charge — ~8× an ESP32 deep-sleep, which on this board leaks ~mA through always-on rails. (Measured on hardware: ~2.15 mV/h vs ~17 mV/h for deep-sleep.) On-demand wake is the power button #4 (the chip is fully off between cycles, so no ESP32-GPIO button can wake it). USB plug/unplug switches modes automatically. While shut down on battery the device shows offline in HA between wakes — normal, not a fault.
- On-device battery indicator (top-right) with a charging bolt when on USB.
- Status-LED feedback (the 2 on-board RGB LEDs): 🟢 refresh · 🟡 prev · 🟣 next · 🔵 booting · 🔴 shutting down.
- Power on/off via the dedicated power button, with an "Off" screen drawn before shutdown (e-ink keeps its last image at zero power, so it must say "off").
- HA-adopted over the ESPHome API: battery %, WiFi, uptime, button events, a Refresh button.
You need: an M5Stack PaperColor (C151), a Home Assistant instance, and a Lovelace→PNG renderer (the Puppet add-on is easiest).
- Flash the firmware. With mainline ESPHome (or the HA ESPHome add-on), create
secrets.yaml, then compile + upload (USB or OTA) — see Building + Secrets. - Make the dashboard. Install the renderer, create a Lovelace dashboard whose URL path matches
puppet_base(e.g.eink-m5paper) usingeink_m5paper.example.jsonas a starting point, then setpuppet_baseinm5papercolor.yamlto point at it — see Dashboard. (Optional: addweather_forecast.example.yamlfor the weather page.) - (Optional) Adopt in Home Assistant. Settings → Devices & Services → ESPHome → add the device's IP + the API key, to get battery %, button events, and a Refresh button as HA entities.
The panel then pulls the rendered dashboard every few minutes, and the buttons page through it (refresh / prev / next). Mind the Caveats — a full e-ink refresh takes ~15–20 s.
Device: M5Stack PaperColor (C151) — official product page · docs (specs, schematics, photos of the unit).
Stand: a 3D-printable stand (the one in the photo) is in hardware/papercolor-stand.3mf (Bambu Studio project).
- MCU: ESP32-S3R8 (16 MB flash / 8 MB OPI-PSRAM)
- Display: 4″ Spectra-6 (6-ink) e-paper, 400×600 (600×400 landscape via
rotation: 90), full-refresh only (~15–20 s, flashing), no partial refresh, no touch - PMIC: M5PM1 (I²C
0x6E, bus SDA=GPIO3 / SCL=GPIO2) - EPD pins: CS 44 · DC 43 · RST 12 · BUSY 11 (inverted) · CLK 15 · MOSI 13
- Buttons: 3 user buttons on GPIO 10 / 9 / 1 (active-low) + a PMIC power button
- RGB LEDs: 2× WS2812-style on GPIO 21
- Device id
0x2050at reg0x00. EPD power = PMIC-GPIO0, SD power = PMIC-GPIO3 (set via I²C). - Charging + rails:
PWR_CFG (0x06)bit0 CHG, bit1 DCDC, bit2 LDO — auto-clear on reset, so the firmware re-sets them each boot. - Power-hold (survive USB-unplug):
HOLD_CFG (0x07)bit5 LDO + bit0 G0 + bit3 G3. - Battery voltage: VBAT regs
0x22/0x23are a full 16-bit mV value (do not mask the high byte). VIN0x24/0x25≈ 5 V on USB → charge detection. - Power button: reported via
BTN_STATUS (0x48)bit7 (pressed-flag) — not the IRQ3 (0x42) click events. Shutdown =SYS_CMD (0x0C) = 0xA1.
Builds with mainline ESPHome ≥ 2026.5.2 (when the epaper_spi Spectra-E6 model landed) —
no fork needed. The easiest path is the Home Assistant ESPHome add-on: add a device, paste
m5papercolor.yaml, drop the components/m5pm1_power/ folder next to it (or point
external_components at github://PFalko/m5stack-papercolor-esphome), add your secrets.yaml, and
Install.
Or from a CLI:
pip install "esphome>=2026.5.2" # or: uv tool install esphome
# create secrets.yaml first (see Secrets below) — compile fails without it
esphome compile m5papercolor.yaml
esphome upload m5papercolor.yaml --device <device-ip> # OTA; USB COMx also worksNotes:
- Framework = Arduino (
esp-idffailed to build in an isolated/uv env here). - If a CMake build dies with a
managed_componentspermission error, delete the build'smanaged_components/+dependencies.lockand recompile. - Provide
secrets.yaml(see Secrets).
Point puppet_base (in substitutions:) at your renderer + dashboard slug — the slug must match
the Lovelace dashboard URL path you create (e.g. eink-m5paper, and 10000 is the Puppet add-on's
default port). The firmware fetches, for each carousel page (panel/energy/climate/garden/weather):
<puppet_base>/<page>?viewport=600x400&format=png&wait=4000&zoom=1.6&colors=<6 inks>
The 6 colors are the panel's native inks so the renderer dithers server-side to exactly what the
panel can show. (wait=4000 gives the dashboard time to render before the screenshot.)
eink_m5paper.example.json— a starter 5-view dashboard (replace the placeholder entities).weather_forecast.example.yaml— optional HA package: a trigger-based template sensor that callsweather.get_forecastsand exposes adailyattribute (the weather entity has no forecast attribute, and a markdown card can't call services), so the weather page can show a big forecast.
- ~15–20 s per refresh (Spectra-6 is full-refresh only). A button press during the e-ink write is dropped (buttons are polled) — press only when the status LED is off.
- The custom PMIC writes are device-specific; wrong register writes can wedge the device (recover with a full power-cycle: unplug, hold power ~10 s, replug).
- Verify the physical↔GPIO button map on your unit — the firmware assumes #1 top = GPIO1,
#2 left-top = GPIO10, #3 right-top = GPIO9; if your silk-screen differs, swap the GPIOs in the
binary_sensor:block. (Button #1's "refresh" re-fetches the current page.)
Create secrets.yaml next to the config with:
wifi_ssid: "YourSSID"
wifi_password: "YourWiFiPassword"
api_key: "base64-32-byte-noise-psk" # e.g. `openssl rand -base64 32`
ota_password: "YourOtaPassword"- Spectra-6
epaper_spisupport: mainline ESPHome'sSpectra-E6model; the PaperColor-specific4.0in-Spectra-E6preset comes from kepstin's PR #16031. - M5PM1 / EPD register maps: M5Stack's open-source M5GFX and M5PM1 libraries.
- Server-rendered dashboard: the Puppet add-on by balloob.
MIT — see LICENSE.



