CYD ESPHome Homeassistant Timer module
Code and stuffs to create a homeassistant timer for the widely available Cheap Yellow Display (CYD) board.
When a timers starts in homeassistant the same timer is displayed on the screen with a timer bar at the bottom. It displays only minutes and seconds as this is used for a 60 minutes timer.
esphome:
name: timer
friendly_name: timer
esp32:
board: esp32dev
framework:
type: arduino
logger:
api:
encryption:
key: "the-secret-api-key"
ota:
- platform: esphome
password: "this-is-a-password"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "Timer Fallback Hotspot"
password: "this-is-a-password"
captive_portal:Paste after captive_portal:
spi:
- id: tft_spi
clk_pin: GPIO14
mosi_pin: GPIO13
miso_pin: GPIO12
globals:
- id: target_timestamp
type: double
initial_value: '0.0'
text_sensor:
- platform: homeassistant
id: s_timer_main_state
entity_id: timer.s_timer
on_value:
then:
- component.update: my_display
- platform: homeassistant
id: s_timer_state
entity_id: timer.s_timer
attribute: remaining
on_value:
then:
- component.update: my_display
- platform: homeassistant
id: s_timer_duration
entity_id: timer.s_timer
attribute: duration
on_value:
then:
- component.update: my_display
- platform: homeassistant
id: s_timer_finishes
entity_id: timer.s_timer
attribute: finishes_at
on_value:
then:
- lambda: |-
int yr = 0, mo = 0, day = 0, hr = 0, min = 0, sec = 0;
if (sscanf(x.c_str(), "%d-%d-%dT%d:%d:%d", &yr, &mo, &day, &hr, &min, &sec) >= 6) {
// 1. Directly normalize month and year fields
mo -= 1;
yr -= 1900;
// 2. Pure programmatic epoch calculation (No external library dependencies)
long days = day - 1;
for (int i = 0; i < mo; ++i) {
const int days_per_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
days += days_per_month[i];
}
// Adjust for leap years
days += (yr - 70) / 4;
if (mo > 1 && (yr % 4 == 0)) days += 1;
long long total_epoch_seconds = (yr - 70) * 365LL + days;
total_epoch_seconds = total_epoch_seconds * 86400LL + hr * 3600LL + min * 60LL + sec;
id(target_timestamp) = (double)total_epoch_seconds;
} else {
id(target_timestamp) = 0.0;
}
- component.update: my_display
font:
- file: "gfonts://Roboto"
id: timer_font
size: 48
output:
- platform: ledc
id: backlight_pwm
pin: GPIO21
light:
- platform: monochromatic
name: "Display Backlight"
id: tft_backlight
output: backlight_pwm
restore_mode: ALWAYS_ON
display:
- platform: ili9xxx
id: my_display
spi_id: tft_spi
model: ILI9342
cs_pin: GPIO15
dc_pin: GPIO2
color_palette: 8BIT
dimensions:
width: 320
height: 240
rotation: 180
invert_colors: true
update_interval: 1s
data_rate: 8000000
lambda: |-
Color bg_color = Color(20, 20, 20);
Color text_active = Color(0, 255, 100);
Color text_paused = Color(255, 140, 0);
Color text_idle = Color(120, 120, 120);
Color bar_fill_color = Color(0, 150, 255);
Color bar_bg_color = Color(60, 60, 60);
it.fill(bg_color);
std::string state = id(s_timer_main_state).state;
std::string remaining = id(s_timer_state).state;
std::string duration_str = id(s_timer_duration).state;
long total_seconds = 0;
long remaining_seconds = 0;
int dur_h = 0, dur_m = 0, dur_s = 0;
if (sscanf(duration_str.c_str(), "%d:%d:%d", &dur_h, &dur_m, &dur_s) >= 2) {
total_seconds = (dur_h * 3600) + (dur_m * 60) + dur_s;
}
if (state == "active") {
double target_epoch = id(target_timestamp);
double now_epoch = id(homeassistant_time).now().timestamp;
if (target_epoch > 0) {
remaining_seconds = (long)(target_epoch - now_epoch);
if (remaining_seconds < 0) remaining_seconds = 0;
int total_m = remaining_seconds / 60;
int s = remaining_seconds % 60;
it.printf(160, 100, id(timer_font), text_active, TextAlign::CENTER, "%02d:%02d", total_m, s);
} else {
it.print(160, 100, id(timer_font), text_active, TextAlign::CENTER, remaining.c_str());
}
}
else if (state == "paused") {
int rem_h = 0, rem_m = 0, rem_s = 0;
if (sscanf(remaining.c_str(), "%d:%d:%d", &rem_h, &rem_m, &rem_s) >= 2) {
remaining_seconds = (rem_h * 3600) + (rem_m * 60) + rem_s;
}
int total_paused_m = (rem_h * 60) + rem_m;
it.printf(160, 100, id(timer_font), text_paused, TextAlign::CENTER, "%02d:%02d", total_paused_m, rem_s);
}
else {
it.print(160, 120, id(timer_font), text_idle, TextAlign::CENTER, "Idle");
}
// Maintained: Right-to-left draining progress bar format
if ((state == "active" || state == "paused") && total_seconds > 0 && remaining_seconds <= total_seconds) {
int bar_x = 0;
int bar_y = 204;
int bar_width = 320;
int bar_height = 36;
float progress = (float)remaining_seconds / (float)total_seconds;
if (progress > 1.0f) progress = 1.0f;
if (progress < 0.0f) progress = 0.0f;
int fill_width = (int)(bar_width * progress);
it.rectangle(bar_x, bar_y, bar_width, bar_height, bar_bg_color);
if (fill_width > 0) {
it.filled_rectangle(bar_x, bar_y, fill_width, bar_height, bar_fill_color);
}
}
time:
- platform: homeassistant
id: homeassistant_timeTo customize the timer bar position:
if ((state == "active" || state == "paused") && total_seconds > 0 && remaining_seconds <= total_seconds) {
int bar_x = 40;
int bar_y = 160;
int bar_width = 240;
int bar_height = 16;