Home » Softronics » CYD ESPHome Homeassistant Timer module & Pinpad

CYD ESPHome Homeassistant Timer module & Pinpad

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.

When a screen is touched, the pinpad activates and waits for input. If no input is given it changes back to the timer.

esphome:
  name: pinpadwithtimer
  friendly_name: PinPadWithTimer

esp32:
  board: esp32dev
  framework:
    type: arduino

logger:
api:
  encryption:
    key: super_secret_api_key
ota:
  - platform: esphome
    password: !secret pinpadwithtimer__ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: !secret pinpadwithtimer__ap_ssid
    password: !secret pinpadwithtimer__ap_password
captive_portal:

Paste after captive_portal:


# ================= BUS CONFIGURATION =================
#  Split the same physical pins into two distinct SPI IDs to stop mipi_spi bus locks
spi:
  # Main Display Hardware SPI Bus
  - id: tft_spi
    clk_pin: GPIO14
    mosi_pin: GPIO13
    miso_pin: GPIO12

  # Separated Touchscreen Hardware SPI Bus
  - id: touch_spi
    clk_pin: GPIO25
    mosi_pin: GPIO32
    miso_pin: GPIO39



globals:
  - id: target_timestamp
    type: double
    initial_value: '0.0'
  - id: current_page
    type: std::string
    initial_value: '"TIMER"'
  - id: entered_code
    type: std::string
    initial_value: '""'
  - id: pad_status
    type: std::string
    initial_value: '"Enter PIN"'
  - id: active_pressed_btn
    type: int
    initial_value: '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

sensor:
  - platform: homeassistant
    id: s_timer_finishes_epoch
    entity_id: timer.s_timer
    attribute: finishes_at
    device_class: timestamp
    on_value:
      then:
        - lambda: |-
            id(target_timestamp) = x;
        - component.update: my_display
          
binary_sensor:
  - platform: template
    name: "PinPad Security Access"
    id: s_pin_success
    device_class: lock

font:
  - file: gfonts://Roboto
    id: timer_font
    size: 48
  - file: gfonts://Roboto
    id: pad_font
    size: 64      # Increased from 28 to make the PIN stars much bigger
  - file: gfonts://Roboto
    id: status_font
    size: 22


output:
  - platform: ledc
    id: backlight_pwm
    pin: GPIO21

light:
  - platform: monochromatic
    name: Display Backlight
    id: tft_backlight
    output: backlight_pwm
    restore_mode: ALWAYS_ON

script:
  # 1. Verifies the security code entered by the user. Change the password here if needed:
  - id: verify_pin
    then:
      - lambda: |-
          if (id(entered_code) == "1234") { 
            id(pad_status) = "Success!";
            id(s_pin_success).publish_state(true); // <--- RESTORED: Sends the ON signal to Home Assistant
          } else {
            id(pad_status) = "Invalid PIN!";
          }
      - component.update: my_display
      - delay: 2s
      - lambda: |-
          id(s_pin_success).publish_state(false); // <--- RESTORED: Automatically resets the HA switch back to OFF
          id(entered_code) = "";
          id(current_page) = "TIMER";
      - component.update: my_display

  # 2. Inactivity watchdog timer for the PIN Pad
  - id: pinpad_timeout_watchdog
    mode: restart
    then:
      - delay: 30s
      - lambda: |-
          if (id(current_page) == "PINPAD") {
            id(entered_code) = "";
            id(pad_status) = "Enter PIN";
            id(current_page) = "TIMER";
            id(my_display).update();
          }



display:
  - platform: mipi_spi
    id: my_display
    spi_id: tft_spi
    model: ILI9342
    cs_pin: GPIO15
    dc_pin: GPIO2
    dimensions:
      width: 320
      height: 240
    rotation: 180
    invert_colors: true
    update_interval: 1s
    data_rate: 8000000
    lambda: |-
      // ================= PAGE 1: TIMER INTERFACE =================
      if (id(current_page) == "TIMER") {
        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 = (double)::time(nullptr); 
            
            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, 90, id(timer_font), text_active, TextAlign::CENTER, "%02d:%02d", total_m, s);
            } else {
                it.print(160, 90, 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, 90, id(timer_font), text_paused, TextAlign::CENTER, "%02d:%02d", total_paused_m, rem_s);
        } else {
            it.print(160, 110, id(timer_font), text_idle, TextAlign::CENTER, "Idle");
        }

        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);
            }
        }
      } 
      
      // ================= PAGE 2: PIN PAD INTERFACE =================
      else if (id(current_page) == "PINPAD") {
        Color bg_color = Color(15, 15, 15);
        Color text_color = Color(255, 255, 255);
        Color btn_default = Color(40, 40, 40);
        Color red = Color(255, 0, 50);          
        
        it.fill(bg_color);
        
        std::string masked(id(entered_code).length(), '*');
        
        it.print(160, 12, id(status_font), text_color, TextAlign::CENTER, id(pad_status).c_str());
        
        // Massive red stars shifted exactly to Y=45
        it.print(160, 45, id(pad_font), red, TextAlign::CENTER, masked.c_str());
        
        // --- Row 1 (Original physical dimensions and positions preserved) ---
        it.filled_rectangle(20, 50, 80, 40, btn_default); 
        it.print(60, 70, id(status_font), text_color, TextAlign::CENTER, "1");
        
        it.filled_rectangle(120, 50, 80, 40, btn_default); 
        it.print(160, 70, id(status_font), text_color, TextAlign::CENTER, "2");
        
        it.filled_rectangle(220, 50, 80, 40, btn_default); 
        it.print(260, 70, id(status_font), text_color, TextAlign::CENTER, "3");
        
        // --- Row 2 ---
        it.filled_rectangle(20, 95, 80, 40, btn_default); 
        it.print(60, 115, id(status_font), text_color, TextAlign::CENTER, "4");
        
        it.filled_rectangle(120, 95, 80, 40, btn_default); 
        it.print(160, 115, id(status_font), text_color, TextAlign::CENTER, "5");
        
        it.filled_rectangle(220, 95, 80, 40, btn_default);
        it.print(260, 115, id(status_font), text_color, TextAlign::CENTER, "6");

        // --- Row 3 ---
        it.filled_rectangle(20, 140, 80, 40, btn_default);
        it.print(60, 160, id(status_font), text_color, TextAlign::CENTER, "7");
        
        it.filled_rectangle(120, 140, 80, 40, btn_default);
        it.print(160, 160, id(status_font), text_color, TextAlign::CENTER, "8");
        
        it.filled_rectangle(220, 140, 80, 40, btn_default);
        it.print(260, 160, id(status_font), text_color, TextAlign::CENTER, "9");

        // --- Row 4 ---
        it.filled_rectangle(20, 185, 80, 40, btn_default);
        it.print(60, 205, id(status_font), text_color, TextAlign::CENTER, "CLR");
        
        it.filled_rectangle(120, 185, 80, 40, btn_default);
        it.print(160, 205, id(status_font), text_color, TextAlign::CENTER, "0");
        
        it.filled_rectangle(220, 185, 80, 40, btn_default);
        it.print(260, 205, id(status_font), text_color, TextAlign::CENTER, "ENT");
      }

touchscreen:
  - platform: xpt2046
    id: cyd_touch
    spi_id: touch_spi
    cs_pin: GPIO33
    # interrupt_pin: GPIO36
    update_interval: 15ms  
    threshold: 80          # Keeps maximum sensitivity active
    touch_timeout: 0ms     
    data_rate: 2000000
    calibration:
      x_min: 200
      x_max: 3900
      y_min: 200
      y_max: 3900
    transform:
      mirror_x: false    
      mirror_y: false    
      swap_xy: true      
    on_touch:
      - script.execute: pinpad_timeout_watchdog  
      - lambda: |-
          if (id(current_page) == "TIMER") {
            id(current_page) = "PINPAD";
            id(entered_code) = "";
            id(pad_status) = "Enter PIN";
            id(my_display).update();
            return; 
          }
          
          static uint32_t last_click = 0;
          uint32_t now = millis();
          if (now - last_click < 150) {
            return;
          }
          
          int x = touch.x;
          int y = touch.y;
          int pressed = 0;
          
          // Row 1 (1, 2, 3)
          if (y >= 50 && y <= 90) {
            if (x >= 20 && x <= 100) { id(entered_code) += "1"; pressed = 1; }
            if (x >= 120 && x <= 200) { id(entered_code) += "2"; pressed = 2; }
            if (x >= 220 && x <= 300) { id(entered_code) += "3"; pressed = 3; }
          }
          // Row 2 (4, 5, 6)
          else if (y >= 95 && y <= 135) {
            if (x >= 20 && x <= 100) { id(entered_code) += "4"; pressed = 4; }
            if (x >= 120 && x <= 200) { id(entered_code) += "5"; pressed = 5; }
            if (x >= 220 && x <= 300) { id(entered_code) += "6"; pressed = 6; }
          }
          // Row 3 (7, 8, 9)
          else if (y >= 140 && y <= 180) {
            if (x >= 20 && x <= 100) { id(entered_code) += "7"; pressed = 7; }
            if (x >= 120 && x <= 200) { id(entered_code) += "8"; pressed = 8; }
            if (x >= 220 && x <= 300) { id(entered_code) += "9"; pressed = 9; }
          }
          // Row 4 (CLR, 0, ENT)
          else if (y >= 185 && y <= 225) {
            if (x >= 20 && x <= 100) { id(entered_code) = ""; pressed = 10; id(pad_status) = "Enter PIN"; } 
            if (x >= 120 && x <= 200) { id(entered_code) += "0"; pressed = 11; }
            if (x >= 220 && x <= 300) {
              pressed = 12;
              id(pad_status) = "Verifying...";
              id(verify_pin).execute();
            }
          }
          
          if (pressed > 0) {
            last_click = now;  
            
            if (id(entered_code).length() == 4 && pressed != 10 && pressed != 12) {
              id(pad_status) = "Verifying...";
              id(verify_pin).execute();
            } else {
              id(my_display).update(); // Redraws top area status instantly
            }
          }


time:
  - platform: homeassistant
    id: homeassistant_time

To 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;

This part is the part, at the very beginning of the -script, where the password can be changed:

- id: verify_pin
    then:
      - lambda: |-
          if (id(entered_code) == "1234")

This part, at the end of the code block, is where the password length can be changed. Change the number 4 to desired length:

if (pressed > 0) {
            last_click = now;  
            
            if (id(entered_code).length() == 4 && pressed != 10 && pressed != 12) {
              id(pad_status) = "Verifying...";
              id(verify_pin).execute();
            } else {
              id(my_display).update(); // Redraws top area status instantly
            }