display_partial() drops calls when the e-paper is still busy, which silently lost HA-triggered widget updates (e.g. fan speed) that landed during the previous refresh's busy window. The retry fires once the busy window has cleared so the state-synced state makes it to the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Waveshare 2.9" Touch ePaper Panel
ESPHome project for the Waveshare 2.9inch Touch e-Paper HAT on an ESP32-WROOM-32 (NodeMCU-32S). Drives an LVGL UI with 2-bit grayscale rendering and touchscreen input.
Hardware
- MCU: ESP32-WROOM-32 (NodeMCU-32S)
- Display: SSD1680, 296×128, 2-bit grayscale via custom component
- Touchscreen: GT1151 / ICNT86X (I2C, polling mode)
Wiring
| Function | ESP Pin | HAT Pin |
|---|---|---|
| Display SCLK | GPIO18 | Pin23/GPIO11/SPI0_SCLK |
| Display MOSI | GPIO23 | Pin19/GPIO10/SPI0_MOSI |
| Display CS | GPIO19 | Pin24/GPIO8/SPI0_CE0 |
| Display DC | GPIO17 | Pin22/GPIO25 |
| Display BUSY | GPIO4 | Pin18/GPIO24 |
| Display RST | GPIO5 | Pin11/GPIO17/SPI1_CE1 |
| Touch SDA | GPIO21 | Pin3/GPIO2/I2C1_SDA |
| Touch SCL | GPIO22 | Pin5/GPIO3/I2C1_SCL |
| Touch RST | GPIO15 | Pin13/GPIO27 |
| Touch INT | GPIO16 | Pin15/GPIO22 |
Project Structure
waveshare-panel/
├── waveshare-test.yaml # Main ESPHome config (edit LVGL layout here)
├── secrets.yaml # WiFi/API credentials (not committed)
├── FreeSans.ttf # Font used in LVGL
└── custom_components/
├── waveshare_epaper_2bit/ # 2-bit grayscale display driver
└── icnt86x/ # ICNT86X touchscreen driver
Build Environment Setup
1. Install Python 3.11
ESPHome 2026.1.0 requires Python 3.11. Check your version:
python3 --version
On Ubuntu/Debian, install it if needed:
sudo apt update
sudo apt install python3.11 python3.11-venv python3.11-dev
On macOS with Homebrew:
brew install python@3.11
2. Create a virtual environment for ESPHome 2026.1.0
Using a dedicated venv keeps this version isolated from other ESPHome installs.
python3.11 -m venv ~/esphome-env/esphome-2026.1.0
source ~/esphome-env/esphome-2026.1.0/bin/activate
3. Install ESPHome 2026.1.0
pip install esphome==2026.1.0
Verify the install:
esphome version
# Expected: Version: 2026.1.0
4. Activate the environment (each session)
source ~/esphome-env/esphome-2026.1.0/bin/activate
You can add this to your shell profile or run it manually before working on the project.
Building and Flashing
All commands assume the venv is active and you are in the project directory.
Compile only
esphome compile waveshare-test.yaml
Flash via USB (first flash or if OTA fails)
esphome upload waveshare-test.yaml
Connect the ESP32 via USB before running. You may need to hold the BOOT button during flashing if the device doesn't enter download mode automatically.
Flash via OTA (subsequent flashes)
Once the device is on WiFi, OTA upload works without USB:
esphome upload waveshare-test.yaml
ESPHome will automatically attempt OTA if the device is reachable.
Monitor serial output
esphome logs waveshare-test.yaml
Or via USB:
esphome logs --device /dev/ttyUSB0 waveshare-test.yaml
Secrets
Copy the secrets template and fill in your credentials:
cp secrets.yaml.example secrets.yaml # if an example exists, otherwise create it
secrets.yaml format:
wifi_ssid: "YourNetworkName"
wappw: "YourWiFiPassword"
enckey: "<32-byte base64 Home Assistant API key>"
apipw: "YourOTAPassword"
hotspotpw: "FallbackPassword"
The fallbackssid variable is set in waveshare-test.yaml and does not go in secrets.
Making Layout Changes
The LVGL UI is defined in waveshare-test.yaml under the lvgl: block. The display renders at 296×128 pixels in a 4-shade grayscale palette:
| LVGL color | Shade |
|---|---|
0xFFFFFF |
White |
0xAAAAAA |
Light grey |
0x555555 |
Dark grey |
0x000000 |
Black |
After editing the YAML, compile and flash:
esphome upload waveshare-test.yaml
The display uses a two-tier refresh:
- Partial refresh (fast, 1-bit) — triggers immediately after any draw event for interactive feedback
- Full refresh (2-bit grayscale quality) — triggers 10 seconds after the last draw event
Full refresh restores proper 4-shade rendering after the fast partial updates.