ESPHome and 2.13" e-ink display

I’m trying to get ESPHome working on an 2.13 mono e-ink display, it’s working in Arduino so hardware is ok, I’cant get it to work in ESPHome , has somebody had this problem and maybe a solution, much appreciated!

1- The driver board for the screen is an ePaper Driver Board for Seeed Studio XIAO with a fixed GPIO pin layout, as shown in the photo.

2- It houses a Seeed Studio XIAO ESP32 C3.

3- 2.13" Monochrome eInk / ePaper Display with 122x250 Pixels, SPI inte rface

The 2.13 inch E-paper Display offers a high-quality, low-power solution for a wide range of applications. The model features a 122x250 resolution, black and white display with a diagonal size of 2.13 inches

Take note that the driver board’s fixed GPIO pin assignments:

GPIO2 D0 = RST on dri ver board

GPIO3 D1 = CS on dr iver board

GPIO4 D2 = BUSY on d river board

GPIO5 D3 = DC on driver board

GPIO10 D10 = MOSI on driver board

GPIO8 D8 = SCK on driver board

Since the display works in Arduino, the hardware is probably fine. I’d first verify the controller IC matches the ESPHome driver (SSD1680, UC8151, IL3897, etc.), then double-check the SPI and control pin assignments. If you can share your ESPHome YAML and the controller part number, it’ll be much easier to identify the issue.

Hi there

So, can you show the back please )(sticker) ?

GL :slight_smile: PJ :v:

I have two differing part numbers…So there’s that :grin:
I know they switched mid stream from Gooddisplay to Waveshare or Visa, versa
at some point.

UPDATE 27/07/26 THIS SCRIPT BELOW IS NOW WORKING

esphome:
name: “co2-meter-slaapkamer”

esp32:
board: esp32-c3-devkitm-1
framework:
type: esp-idf
variant: ESP32C3

#Ensures logs continue to stream via the USB-C port under ESP-IDF

logger:
hardware_uart: USB_CDC
level: DEBUG

wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip:
static_ip: “192.168.0.53”
gateway: “192.168.0.1”
subnet: “255.255.255.0”
dns1: “1.1.1.1”
dns2: “8.8.8.8”
fast_connect: true
output_power: 10.0dBm

ap:
ssid: “CO2-Display Fallback AP”
password: “xxxxx”

ota:

  • platform: esphome

captive_portal:

api:
encryption:
key: “xxxxxxxxxxxxxxxxxxxx=”

web_server:
port: 80
version: 2
local: true
include_internal: true

#Define the fonts used on the screen

font:

  • file:
    type: gfonts
    family: “Open Sans”
    weight: bold
    id: sans_bold_12
    size: 12

  • file:
    type: gfonts
    family: “Open Sans”
    weight: bold
    id: sans_bold_24
    size: 24

sensor:

platform: homeassistant
entity_id: sensor.co2_meter_slaapkamer_scd41_co2
id: ha_co2_sensor
internal: true

#Force a screen refresh as soon as new data arrives

  • on_value:
    then:

    • component.update: epaper_display

#Bitbang software SPI to eliminate strapping pin conflicts on GPIO8

spi:
clk_pin:
number: 8
mode: output
mosi_pin:
number: 10
mode: output

epaper Display Configuration (SSD1680)

display:

  • platform: waveshare_epaper
    id: epaper_display
    cs_pin: 3
    dc_pin: 5
    reset_pin: 2
    busy_pin: 4
    model: 2.13in-ttgo-b74
    rotation: 90
    update_interval: 300s

    lambda: |-
    // Clear background to white
    it.fill(Color::WHITE);

    // 1. Draw header text
    it.print(20, 0, id(sans_bold_12), Color::BLACK, “CO2 Bedroom:”);

    // Check if the sensor has received valid data from HA
    if (std::isnan(id(ha_co2_sensor).state)) {
    // Status when no data is loaded yet or Wi-Fi is disconnected
    it.printf(20, 35, id(sans_bold_24), Color::BLACK, “Loading…”);
    it.print(20, 90, id(sans_bold_12), Color::BLACK, “Status: Unknown”);
    } else {
    // Fetch numerical value from the Home Assistant state
    int co2_numeric = (int)id(ha_co2_sensor).state;

    // 2. Print large CO2 value with "ppm" suffix
    it.printf(20, 35, id(sans_bold_24), Color::BLACK, "%d ppm", co2_numeric);
    
    // 3. Determine and display indoor air quality status
    if (co2_numeric < 800) {
      it.print(20, 90, id(sans_bold_12), Color::BLACK, "Status: Good");
    } else if (co2_numeric >= 800 && co2_numeric <= 1200) {
      it.print(20, 90, id(sans_bold_12), Color::BLACK, "Status: Fair");
    } else {
      it.print(20, 90, id(sans_bold_12), Color::BLACK, "Status: Poor!");
    }
    

    }

Update 25/0726:

This script is now working, now on refining!!!

Good to know that. Cheers.

Great progress. For refinement, I would avoid refreshing the e-ink display on every Home Assistant update, as frequent full refreshes can increase ghosting and power consumption. Consider updating only when the CO2 value changes significantly … perhaps by 25–50 ppm …or after a maximum time interval.

You could also add a “last updated” time and Wi-Fi/API status indicator, which would make stale readings easier to identify.

Thanx for the info, will make the changes

Hi There,

So, Great suggestion on dialing back the update frequency to save power! You are absolutely right that running frequent updates will drain the battery much faster. :+1:

However, I did want to add a quick technical note regarding the display ghosting: it’s actually the partial refreshes that cause ghosting to build up over time. A full black-and-white refresh cycle is what the panel uses to clear out the residual charge (DC bias) :backhand_index_pointing_left: :wink: and restore the capsules.

Manufacturers specifically advise forcing a full refresh to clear these artifacts. For example, Good Display’s E-paper Display FAQ states: “we suggest that after partial refresh for 5 times, it is necessary to add a black-and-white full screen refresh display… to improve ghosting.”

But your main point is spot on :bullseye: updating only when the CO2 shifts by 25–50 ppm is a much better approach for the longevity of the battery and the panel!

I have found this to be true with all the Seeed E-Papers I have tested and posted demo’s on. even though it’s a 6 year or more old tech, the controllers and panels are getting better and better. the 1.54" is great, seeed should offer it with a touch plane. :crossed_fingers:

HTH
GL :slight_smile: PJ :v:

still waiting on the E1003 i won in live stream a while back. :grin: