Skip to content

LCD Touch Panel Timer Display

This example demonstrates how to create a dedicated timer display panel using an ESP32 with a touchscreen LCD. Unlike the full voice assistant configurations, this is a display-only device that shows timer status from Home Assistant and allows basic touch controls.

Overview

LCD Touch Timer Panel

Artist's impression - actual display may vary based on your screen and configuration.

Features:

  • Large, readable countdown display
  • Touch buttons to pause/resume and cancel timers
  • Progress bar showing remaining time
  • Multiple timer view (optional)
  • Color-coded states

Hardware Requirements

  • ESP32 or ESP32-S3 development board
  • ILI9341/ILI9xxx LCD display (240x320 or 320x480)
  • XPT2046 or GT911 touchscreen controller
  • WiFi connectivity to Home Assistant

Tested Hardware

  • ESP32-S3 + 2.8" ILI9341 with XPT2046 touchscreen
  • ESP32-S3 + 3.5" ILI9488 with GT911 touchscreen
  • ESP32-S3 development boards

Configuration

Substitutions

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: substitutions
substitutions:
  device_name: "timer-panel"
  friendly_name: "Timer Panel"

  # Area to display - set to your primary area
  timer_area: "kitchen"

  # Display configuration
  display_width: "320"
  display_height: "240"
  display_rotation: "0" # 0, 90, 180, 270

  # Colors (as hex without #)
  color_active: "00C853" # Green
  color_paused: "2979FF" # Blue
  color_finished: "FF1744" # Red
  color_idle: "757575" # Gray
  color_background: "000000" # Black
  color_text: "FFFFFF" # White

  # SPI pins for display (adjust for your board)
  display_clk: GPIO18
  display_mosi: GPIO23
  display_miso: GPIO19
  display_cs: GPIO5
  display_dc: GPIO16
  display_rst: GPIO17
  display_backlight: GPIO4

  # Touch pins (XPT2046)
  touch_clk: GPIO18
  touch_mosi: GPIO23
  touch_miso: GPIO19
  touch_cs: GPIO21
  touch_irq: GPIO36

ESPHome Core

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: esphome
esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  on_boot:
    priority: 600
    then:
      - light.turn_on:
          id: backlight
          brightness: 80%
      - script.execute: update_display

ESP32 Platform

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: esp32
esp32:
  board: esp32dev
  framework:
    type: arduino

WiFi, Logger, API

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: wifi
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  on_connect:
    - script.execute: update_display
  on_disconnect:
    - script.execute: update_display

logger:
  level: INFO

api:
  on_client_connected:
    - script.execute: update_display
  on_client_disconnected:
    - script.execute: update_display

ota:
  - platform: esphome

SPI Bus Configuration

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: spi
spi:
  clk_pin: ${display_clk}
  mosi_pin: ${display_mosi}
  miso_pin: ${display_miso}

Home Assistant Timer Sensors

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: sensor
sensor:
  # Timer remaining seconds
  - platform: homeassistant
    id: timer_remaining
    name: "Timer Remaining"
    entity_id: sensor.${timer_area}_timer_remaining_seconds
    unit_of_measurement: "s"
    on_value:
      then:
        - script.execute: update_display

  # Timer total duration
  - platform: homeassistant
    id: timer_duration
    entity_id: sensor.${timer_area}_timer_remaining_seconds
    attribute: duration_seconds
    internal: true

  # Timer progress percentage
  - platform: homeassistant
    id: timer_progress
    entity_id: sensor.${timer_area}_timer_remaining_seconds
    attribute: progress_percent
    internal: true

Timer State Text Sensor

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: text_sensor
text_sensor:
  - platform: homeassistant
    id: timer_state
    entity_id: sensor.${timer_area}_timer_remaining_seconds
    attribute: timer_state
    internal: true
    on_value:
      then:
        - script.execute: update_display

Display Refresh Interval

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: interval
interval:
  # Regular refresh
  - interval: 1s
    then:
      - script.execute: update_display

  # Fast refresh in final minute
  - interval: 100ms
    then:
      - lambda: |-
          std::string state = id(timer_state).state;
          float remaining = id(timer_remaining).state;
          if ((state == "active") && !std::isnan(remaining) && remaining <= 60) {
            id(update_display).execute();
          }

Backlight Control

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: light
output:
  - platform: ledc
    pin: ${display_backlight}
    id: backlight_pwm

light:
  - platform: monochromatic
    id: backlight
    name: "Display Backlight"
    output: backlight_pwm
    restore_mode: RESTORE_DEFAULT_ON
    default_transition_length: 250ms

Touchscreen Configuration

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: touchscreen
touchscreen:
  - platform: xpt2046
    id: touch_screen
    cs_pin: ${touch_cs}
    interrupt_pin: ${touch_irq}
    calibration:
      x_min: 280
      x_max: 3860
      y_min: 340
      y_max: 3860
    transform:
      swap_xy: false
      mirror_x: false
      mirror_y: false

Touch Buttons (Binary Sensors)

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: binary_sensor
binary_sensor:
  # Pause/Resume button (left side)
  - platform: touchscreen
    id: btn_pause
    x_min: 20
    x_max: 140
    y_min: 200
    y_max: 230
    on_press:
      then:
        - lambda: |-
            std::string state = id(timer_state).state;
            if (state == "active") {
              // Pause the timer
              id(pause_timer).execute();
            } else if (state == "paused") {
              // Resume the timer
              id(resume_timer).execute();
            }

  # Cancel button (right side)
  - platform: touchscreen
    id: btn_cancel
    x_min: 180
    x_max: 300
    y_min: 200
    y_max: 230
    on_press:
      then:
        - lambda: |-
            std::string state = id(timer_state).state;
            if (state == "active" || state == "paused") {
              id(cancel_timer).execute();
            }

Home Assistant Service Scripts

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: script
script:
  - id: update_display
    mode: restart
    then:
      - component.update: lcd_display

  - id: pause_timer
    then:
      - homeassistant.service:
          service: timer.pause
          data:
            entity_id: timer.${timer_area}

  - id: resume_timer
    then:
      - homeassistant.service:
          service: timer.start
          data:
            entity_id: timer.${timer_area}

  - id: cancel_timer
    then:
      - homeassistant.service:
          service: timer.cancel
          data:
            entity_id: timer.${timer_area}

Fonts

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: font
font:
  # Extra large for countdown
  - file:
      type: gfonts
      family: Roboto Mono
      weight: 700
    id: font_countdown
    size: 48
    glyphs: "0123456789:"

  # Large for labels
  - file:
      type: gfonts
      family: Roboto
      weight: 500
    id: font_large
    size: 24
    glyphs: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:.- "

  # Medium for status
  - file:
      type: gfonts
      family: Roboto
      weight: 400
    id: font_medium
    size: 18
    glyphs: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:.- "

  # Small for buttons
  - file:
      type: gfonts
      family: Roboto
      weight: 500
    id: font_button
    size: 14
    glyphs: "ABCDEFGHIJKLMNOPQRSTUVWXYZ "

Colors

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: color
color:
  - id: color_timer_active
    hex: ${color_active}
  - id: color_timer_paused
    hex: ${color_paused}
  - id: color_timer_finished
    hex: ${color_finished}
  - id: color_timer_idle
    hex: ${color_idle}
  - id: color_bg
    hex: ${color_background}
  - id: color_text_primary
    hex: ${color_text}
  - id: color_progress_bg
    hex: "333333"
  - id: color_button_bg
    hex: "444444"
  - id: color_button_border
    hex: "666666"

Display Configuration

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: display
display:
  - platform: ili9xxx
    id: lcd_display
    model: ILI9341
    cs_pin: ${display_cs}
    dc_pin: ${display_dc}
    reset_pin: ${display_rst}
    rotation: ${display_rotation}
    update_interval: never
    lambda: |-
      // Display dimensions
      int width = it.get_width();
      int height = it.get_height();
      int center_x = width / 2;

      // Get timer data
      std::string state = id(timer_state).state;
      float remaining_f = id(timer_remaining).state;
      float duration_f = id(timer_duration).state;
      float progress_f = id(timer_progress).state;

      int remaining = std::isnan(remaining_f) ? 0 : (int)remaining_f;
      int duration = std::isnan(duration_f) ? 0 : (int)duration_f;
      float progress = std::isnan(progress_f) ? 0 : progress_f;

      // Clear background
      it.fill(id(color_bg));

      // ═══════════════════════════════════════════════════════
      // Connection Status Check
      // ═══════════════════════════════════════════════════════
      if (!id(wifi_id).is_connected()) {
        it.printf(center_x, height / 2, id(font_large), id(color_timer_finished),
                  TextAlign::CENTER, "No WiFi");
        return;
      }
      if (!id(api_id).is_connected()) {
        it.printf(center_x, height / 2, id(font_large), id(color_timer_idle),
                  TextAlign::CENTER, "Connecting to HA...");
        return;
      }

      // Determine state color
      Color state_color;
      if (state == "active") {
        state_color = id(color_timer_active);
      } else if (state == "paused") {
        state_color = id(color_timer_paused);
      } else {
        state_color = id(color_timer_idle);
      }

      // ═══════════════════════════════════════════════════════
      // Timer Active or Paused
      // ═══════════════════════════════════════════════════════
      if (state == "active" || state == "paused") {
        // Area label at top
        it.printf(center_x, 10, id(font_medium), id(color_text_primary),
                  TextAlign::TOP_CENTER, "${timer_area} Timer");

        // Format countdown time
        int hours = remaining / 3600;
        int minutes = (remaining % 3600) / 60;
        int seconds = remaining % 60;

        char time_str[16];
        if (hours > 0) {
          snprintf(time_str, sizeof(time_str), "%d:%02d:%02d", hours, minutes, seconds);
        } else {
          snprintf(time_str, sizeof(time_str), "%02d:%02d", minutes, seconds);
        }

        // Large countdown display (centered)
        it.printf(center_x, 70, id(font_countdown), state_color,
                  TextAlign::CENTER, "%s", time_str);

        // Status label
        const char* status_label = (state == "paused") ? "PAUSED" : "REMAINING";
        it.printf(center_x, 110, id(font_medium), state_color,
                  TextAlign::TOP_CENTER, "%s", status_label);

        // Progress bar
        int bar_x = 20;
        int bar_y = 150;
        int bar_width = width - 40;
        int bar_height = 20;

        // Background
        it.filled_rectangle(bar_x, bar_y, bar_width, bar_height, id(color_progress_bg));

        // Fill
        int fill_width = (int)((progress / 100.0f) * bar_width);
        if (fill_width > 0) {
          it.filled_rectangle(bar_x, bar_y, fill_width, bar_height, state_color);
        }

        // Border
        it.rectangle(bar_x, bar_y, bar_width, bar_height, id(color_button_border));

        // Progress percentage
        it.printf(center_x, bar_y + bar_height + 5, id(font_medium), id(color_text_primary),
                  TextAlign::TOP_CENTER, "%.0f%%", progress);

        // ═══════════════════════════════════════════════════════
        // Touch Buttons
        // ═══════════════════════════════════════════════════════

        // Pause/Resume button (left)
        int btn_y = 200;
        int btn_height = 30;
        int btn_width = 120;

        it.filled_rectangle(20, btn_y, btn_width, btn_height, id(color_button_bg));
        it.rectangle(20, btn_y, btn_width, btn_height, state_color);

        const char* pause_label = (state == "paused") ? "RESUME" : "PAUSE";
        it.printf(20 + btn_width / 2, btn_y + btn_height / 2, id(font_button), state_color,
                  TextAlign::CENTER, "%s", pause_label);

        // Cancel button (right)
        it.filled_rectangle(180, btn_y, btn_width, btn_height, id(color_button_bg));
        it.rectangle(180, btn_y, btn_width, btn_height, id(color_timer_finished));
        it.printf(180 + btn_width / 2, btn_y + btn_height / 2, id(font_button),
                  id(color_timer_finished), TextAlign::CENTER, "CANCEL");

      // ═══════════════════════════════════════════════════════
      // Idle State
      // ═══════════════════════════════════════════════════════
      } else {
        // Show ready state
        it.printf(center_x, 60, id(font_large), id(color_timer_idle),
                  TextAlign::CENTER, "${timer_area}");

        it.printf(center_x, 100, id(font_medium), id(color_timer_idle),
                  TextAlign::CENTER, "Timer Ready");

        it.printf(center_x, 150, id(font_medium), id(color_text_primary),
                  TextAlign::CENTER, "Use voice command or");
        it.printf(center_x, 175, id(font_medium), id(color_text_primary),
                  TextAlign::CENTER, "HA to start a timer");
      }

WiFi and API IDs

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: wifi_api_ids
wifi:
  id: wifi_id
  ssid: !secret wifi_ssid
  password: !secret wifi_password

api:
  id: api_id

Multi-Timer Display

For displaying multiple timers on a larger screen, you can modify the display lambda:

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: multi_timer_display
# Additional sensors for multiple areas
sensor:
  # Kitchen timer
  - platform: homeassistant
    id: timer_kitchen_remaining
    entity_id: sensor.kitchen_timer_remaining_seconds
    internal: true

  # Bedroom timer
  - platform: homeassistant
    id: timer_bedroom_remaining
    entity_id: sensor.bedroom_timer_remaining_seconds
    internal: true

  # Playroom timer
  - platform: homeassistant
    id: timer_playroom_remaining
    entity_id: sensor.playroom_timer_remaining_seconds
    internal: true

text_sensor:
  - platform: homeassistant
    id: timer_kitchen_state
    entity_id: sensor.kitchen_timer_remaining_seconds
    attribute: timer_state
    internal: true

  - platform: homeassistant
    id: timer_bedroom_state
    entity_id: sensor.bedroom_timer_remaining_seconds
    attribute: timer_state
    internal: true

  - platform: homeassistant
    id: timer_playroom_state
    entity_id: sensor.playroom_timer_remaining_seconds
    attribute: timer_state
    internal: true

Then create a multi-timer display lambda that shows all active timers in a list format.

GT911 Touch Alternative

For displays with GT911 capacitive touch (like many ESP32-S3 displays):

# file: esphome/examples/lcd-touch-timer-panel.yaml
# section: gt911_touch
i2c:
  sda: GPIO21
  scl: GPIO22
  scan: true

touchscreen:
  - platform: gt911
    id: touch_screen
    interrupt_pin: GPIO4
    # No calibration needed for capacitive touch

Testing

  1. Flash the ESP32 with the configuration
  2. Verify WiFi connection and Home Assistant API connection
  3. Start a timer using a voice assistant or Home Assistant
  4. Verify the countdown appears on the display
  5. Test the PAUSE button - timer should pause and button label change to RESUME
  6. Test the CANCEL button - timer should stop and display return to idle
  7. Let a timer complete naturally to verify all states

Customization Ideas

  • Multiple pages: Add swipe gestures to switch between areas
  • Quick timers: Add preset buttons for common durations (5, 10, 15 min)
  • History: Show recently completed timers
  • Themes: Add day/night themes based on time or ambient light
  • Sound: Add a piezo buzzer for local alarm feedback