substitutions: name: cat-medication-tracker friendly_name: "Cat Medication Tracker" esphome: name: ${name} friendly_name: ${friendly_name} esp32: board: esp32dev framework: type: arduino # Enable logging logger: # Enable Home Assistant API api: encryption: key: !secret api_encryption_key ota: platform: esphome password: !secret ota_password wifi: ssid: !secret wifi_ssid password: !secret wifi_password ap: ssid: "${name}-fallback" password: !secret fallback_password captive_portal: # Time component for midnight reset time: - platform: homeassistant id: homeassistant_time on_time: # Reset at midnight - seconds: 0 minutes: 0 hours: 0 then: - switch.turn_off: penelope_medicated - switch.turn_off: tess_medicated - script.execute: update_display # SPI for display and touchscreen spi: - id: tft_spi clk_pin: GPIO14 mosi_pin: GPIO13 miso_pin: GPIO12 # ILI9341 Display (2.4" 240x320) display: - platform: ili9xxx model: ILI9341 spi_id: tft_spi cs_pin: GPIO15 dc_pin: GPIO2 reset_pin: GPIO4 rotation: 0 invert_colors: false dimensions: width: 240 height: 320 id: my_display lambda: |- // Colors auto red = Color(255, 0, 0); auto green = Color(0, 200, 0); auto light_grey = Color(200, 200, 200); auto white = Color(255, 255, 255); auto black = Color(0, 0, 0); auto dark_grey = Color(80, 80, 80); // Fill background with light grey it.fill(light_grey); // Border color: green if all cats medicated, red otherwise bool all_done = id(penelope_medicated).state && id(tess_medicated).state; auto border_color = all_done ? green : red; // Draw border (8 pixels thick) int border = 8; it.filled_rectangle(0, 0, 240, border, border_color); // Top it.filled_rectangle(0, 320 - border, 240, border, border_color); // Bottom it.filled_rectangle(0, 0, border, 320, border_color); // Left it.filled_rectangle(240 - border, 0, border, 320, border_color); // Right // Title it.printf(120, 25, id(title_font), black, TextAlign::TOP_CENTER, "Cat Meds"); // Penelope button (top button) int btn_x = 30; int btn_y = 60; int btn_w = 180; int btn_h = 80; auto penelope_color = id(penelope_medicated).state ? green : red; it.filled_rectangle(btn_x, btn_y, btn_w, btn_h, penelope_color); it.rectangle(btn_x, btn_y, btn_w, btn_h, dark_grey); it.printf(btn_x + btn_w/2, btn_y + btn_h/2, id(button_font), white, TextAlign::CENTER, "Penelope"); if (id(penelope_medicated).state) { it.printf(btn_x + btn_w/2, btn_y + btn_h - 15, id(status_font), white, TextAlign::CENTER, "DONE"); } // Tess button (middle button) btn_y = 155; auto tess_color = id(tess_medicated).state ? green : red; it.filled_rectangle(btn_x, btn_y, btn_w, btn_h, tess_color); it.rectangle(btn_x, btn_y, btn_w, btn_h, dark_grey); it.printf(btn_x + btn_w/2, btn_y + btn_h/2, id(button_font), white, TextAlign::CENTER, "Tess"); if (id(tess_medicated).state) { it.printf(btn_x + btn_w/2, btn_y + btn_h - 15, id(status_font), white, TextAlign::CENTER, "DONE"); } // Reset button (bottom, smaller) int reset_x = 70; int reset_y = 260; int reset_w = 100; int reset_h = 40; it.filled_rectangle(reset_x, reset_y, reset_w, reset_h, dark_grey); it.rectangle(reset_x, reset_y, reset_w, reset_h, black); it.printf(reset_x + reset_w/2, reset_y + reset_h/2, id(status_font), white, TextAlign::CENTER, "RESET"); # XPT2046 Touchscreen touchscreen: - platform: xpt2046 spi_id: tft_spi cs_pin: GPIO33 interrupt_pin: GPIO36 calibration: x_min: 280 x_max: 3850 y_min: 340 y_max: 3860 on_touch: then: - lambda: |- int touch_x = touch.x; int touch_y = touch.y; // Penelope button bounds: x=30-210, y=60-140 if (touch_x >= 30 && touch_x <= 210 && touch_y >= 60 && touch_y <= 140) { id(penelope_medicated).toggle(); } // Tess button bounds: x=30-210, y=155-235 else if (touch_x >= 30 && touch_x <= 210 && touch_y >= 155 && touch_y <= 235) { id(tess_medicated).toggle(); } // Reset button bounds: x=70-170, y=260-300 else if (touch_x >= 70 && touch_x <= 170 && touch_y >= 260 && touch_y <= 300) { id(penelope_medicated).turn_off(); id(tess_medicated).turn_off(); } - script.execute: update_display # Backlight control output: - platform: ledc pin: GPIO21 id: backlight_pwm light: - platform: monochromatic output: backlight_pwm name: "${friendly_name} Backlight" id: backlight restore_mode: ALWAYS_ON # Medication state switches (exposed to Home Assistant) switch: - platform: template name: "Penelope Medicated" id: penelope_medicated optimistic: true restore_mode: RESTORE_DEFAULT_OFF on_turn_on: - script.execute: update_display on_turn_off: - script.execute: update_display - platform: template name: "Tess Medicated" id: tess_medicated optimistic: true restore_mode: RESTORE_DEFAULT_OFF on_turn_on: - script.execute: update_display on_turn_off: - script.execute: update_display - platform: template name: "Reset All Medications" id: reset_all optimistic: false turn_on_action: - switch.turn_off: penelope_medicated - switch.turn_off: tess_medicated # Binary sensors for Home Assistant dashboard binary_sensor: - platform: template name: "Penelope Medication Status" lambda: 'return id(penelope_medicated).state;' device_class: running - platform: template name: "Tess Medication Status" lambda: 'return id(tess_medicated).state;' device_class: running - platform: template name: "All Cats Medicated" lambda: 'return id(penelope_medicated).state && id(tess_medicated).state;' device_class: running # Script to update display script: - id: update_display then: - component.update: my_display # Fonts font: - file: "gfonts://Roboto" id: title_font size: 28 - file: "gfonts://Roboto" id: button_font size: 24 - file: "gfonts://Roboto" id: status_font size: 14