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¶
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¶
- Flash the ESP32 with the configuration
- Verify WiFi connection and Home Assistant API connection
- Start a timer using a voice assistant or Home Assistant
- Verify the countdown appears on the display
- Test the PAUSE button - timer should pause and button label change to RESUME
- Test the CANCEL button - timer should stop and display return to idle
- 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