# Andrew Villeneuve 2026/05 # Waveshare 2.9inch Touch ePaper HAT integration demo # # Platform: https://www.waveshare.com/2.9inch-Touch-e-Paper-HAT.htm # # Pin mappings # --- # |Function |ESP Pin |HAT Pin |Wire | # |--- |--- |--- |--- | # |5V |VIN |Pin2/5V |Red | # |GND |GND |Pin6/GND |Blk | # |Display SCLK |GPIO18 |Pin23/GPIO11/SPI0_SCLK |Blue | # |Display MOSI |GPIO23 |Pin19/GPIO10/SPI0_MOSI |Green | # |Display CS |GPIO19 |Pin24/GPIO8/SPI0_CE0 |Mgnta | # |Display DC |GPIO17 |Pin22/GPIO25 |White | # |Display Busy |GPIO4 |Pin24/GPIO24 |Brown | # |Display RST |GPIO5 |Pin11/GPIO17/SPI1_CE1 |Orange | # |Touch SDA |GPIO21 |Pin3/GPIO2/I2C1_SDA |Gray | # |Touch SCL |GPIO22 |Pin5/GPIO3/I2C1_SCL |Purple | # |Touch RST |GPIO15 |Pin13/GPIO27 |Blue | # |Touch INT |GPIO16 |Pin15/GPIO22 |White | # # Exposed Entities # --- # ### Boilerplate ### esphome: name: waveshare-epaper-test esp32: board: nodemcu-32s framework: type: esp-idf # Enable logging logger: level: DEBUG # Enable Home Assistant API api: encryption: key: !secret enckey ota: - platform: esphome password: !secret apipw wifi: ssid: !secret wifi_ssid password: !secret wappw # Enable fallback hotspot (captive portal) in case wifi connection fails ap: ssid: ${fallbackssid} password: !secret hotspotpw captive_portal: ### Setup Interfaces ### # Display spi: clk_pin: GPIO18 mosi_pin: GPIO23 display: # This platform is a custom_component forked from upstream waveshare_epaper # but modified by Claude to support 2bpp greyscale - platform: waveshare_epaper_2bit id: display0 cs_pin: GPIO19 dc_pin: GPIO17 busy_pin: GPIO4 reset_pin: GPIO5 model: 2.90inv2-r2-2bpp # model: 2.90inv2-r2 full_update_every: 30 rotation: 270° auto_clear_enabled: false update_interval: never # Touchscreen i2c: id: i2c_bus sda: GPIO21 scl: GPIO22 scan: false touchscreen: # This platform is a custom_component written from scratch by Claude platform: icnt86x id: touchscreen0 address: 0x48 reset_pin: GPIO15 update_interval: 50ms calibration: x_min: 0 x_max: 295 y_min: 0 y_max: 127 transform: mirror_x: true mirror_y: true ### Okay, now the Good Stuff ### # Load some fonts for the display renderer font: - file: "FreeSans.ttf" id: font1 size: 26 bpp: 2 - file: "FreeSans.ttf" id: headerfont size: 80 - file: "FreeSans.ttf" id: console size: 30 # Pull in some icons from the material design library # https://pictogrammers.com/library/mdi/ image: defaults: transparency: chroma_key invert_alpha: false resize: 54x54 binary: - file: "mdi:home-lock-open" id: home_lock_open - file: "mdi:home-lock" id: home_lock - file: "mdi:ceiling-fan" id: ceiling_fan - file: "mdi:ceiling-fan-light" id: ceiling_fan_light - file: "mdi:fan-off" id: fan_off - file: "mdi:fan-speed-1" id: fan_speed_1 - file: "mdi:fan-speed-2" id: fan_speed_2 - file: "mdi:fan-speed-3" id: fan_speed_3 - file: "mdi:floor-lamp-torchiere" id: floor_lamp - file: "mdi:home-off" id: away_button # When the user taps the screen, introduce a small delay for the LVGL framebuffer # to stabilize, then do a quick, 1-bit B/W selective update to visually indicate # that a button was pushed. # # When the user is done interacting with the screen and several seconds have # passed, do a slow, full-screen redraw at 2-bit grayscale. script: - id: refresh_display mode: restart then: - delay: 60ms - lambda: 'id(display0).display_partial();' - id: full_refresh_display mode: restart then: - delay: 10s - component.update: display0 binary_sensor: # Force a full display refresh when we press the "boot" button on the devboard - platform: gpio pin: number: GPIO0 mode: input: true pullup: true inverted: true id: boot_button internal: true on_click: then: - component.update: lvgl0 - delay: 1s - component.update: display0 # List the actual home entitites that we want this panel to control here - platform: homeassistant id: goat_summit_light entity_id: light.goat_summit_light publish_initial_state: true on_state: then: - lvgl.widget.update: id: button_ceiling_fan_light state: checked: !lambda return x; # Here's where we put the actual GUI layout lvgl: - id: lvgl0 displays: - display0 touchscreens: - touchscreen0 on_draw_end: - script.execute: refresh_display - script.execute: full_refresh_display # Black: 0x000000 # Dark Grey: 0x555555 # Light Grey: 0xAAAAAA # White: 0xFFFFFF bg_color: 0x000000 theme: button: bg_color: 0xFFFFFF text_color: 0x000000 height: 60 width: 64 pad_all: 0 border_width: 2 border_color: 0x000000 checked: bg_color: 0x888888 align: CENTER obj: bg_color: 0xFFFFFF widgets: - obj: width: 100% height: 100% scrollable: false pad_all: 0 # pad_row: 0 # pad_column: 0 border_width: 0 bg_color: 0xFFFFFF layout: type: GRID grid_rows: [fr(1), fr(1)] grid_columns: [fr(1), fr(1), fr(1), fr(1)] widgets: - button: id: button_ceiling_fan grid_cell_row_pos: 0 grid_cell_column_pos: 0 on_click: - logger.log: "ceiling_fan" widgets: - image: src: ceiling_fan align: CENTER - button: id: button_ceiling_fan_light grid_cell_row_pos: 0 grid_cell_column_pos: 1 on_click: - logger.log: "ceiling_fan_light" - homeassistant.action: action: light.toggle data: entity_id: light.goat_summit_light checkable: true widgets: - image: src: ceiling_fan_light - button: id: button_fan_off grid_cell_row_pos: 0 grid_cell_column_pos: 2 on_click: - logger.log: "fan_off" checkable: true widgets: - image: src: fan_off - button: id: button_fan_1 grid_cell_row_pos: 0 grid_cell_column_pos: 3 checkable: true on_click: - logger.log: "fan_speed_1" - lvgl.widget.update: id: button_fan_2 state: checked: false - lvgl.widget.update: id: button_fan_3 state: checked: false widgets: - image: src: fan_speed_1 - button: id: button_fan_2 grid_cell_row_pos: 1 grid_cell_column_pos: 0 on_click: - logger.log: "fan_speed_2" checkable: true widgets: - image: src: fan_speed_2 - button: id: button_fan_3 grid_cell_row_pos: 1 grid_cell_column_pos: 1 on_click: - logger.log: "fan_speed_3" checkable: true widgets: - image: src: fan_speed_3 - button: id: button_floor_lamp grid_cell_row_pos: 1 grid_cell_column_pos: 2 on_click: - logger.log: "floor_lamp" checkable: true widgets: - image: src: floor_lamp - button: id: button_away grid_cell_row_pos: 1 grid_cell_column_pos: 3 on_click: - logger.log: "away_button" checkable: true widgets: - image: src: away_button