substitutions: name: cat-medication-tracker friendly_name: "Cat Medication Tracker" # ESP32-32E note: # This board commonly lacks PSRAM, so we rely on a small TFT render buffer. # Keep this display on a reduced buffer/8-bit depth strategy to avoid startup OOM. esphome: name: ${name} friendly_name: ${friendly_name} on_boot: priority: 100 then: # Force backlight on after a short delay so we can confirm it is wired correctly. - light.turn_off: backlight - delay: 300ms - light.turn_on: backlight - delay: 300ms - component.update: my_display esp32: board: esp32dev framework: type: arduino # Enable logging logger: level: DEBUG logs: xpt2046: WARN # Enable Home Assistant API api: encryption: key: !secret api_encryption_key ota: platform: esphome password: !secret ota_password wifi: ssid: !secret wifi_iot_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 # ILI9488 Display (3.5" 320x480, portrait) display: - platform: mipi_spi model: ILI9488 spi_id: tft_spi cs_pin: GPIO15 dc_pin: GPIO2 reset_pin: GPIO4 rotation: 0 invert_colors: true color_order: bgr data_rate: 20MHz dimensions: width: 320 height: 480 id: my_display auto_clear_enabled: false update_interval: 2s color_depth: 16 # Keep memory usage low without PSRAM: draw only a quarter of the screen per chunk. buffer_size: 25% 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 (10 pixels thick) int border = 10; it.filled_rectangle(0, 0, 320, border, border_color); // Top it.filled_rectangle(0, 480 - border, 320, border, border_color); // Bottom it.filled_rectangle(0, 0, border, 480, border_color); // Left it.filled_rectangle(320 - border, 0, border, 480, border_color); // Right // Title it.printf(160, 30, id(title_font), black, TextAlign::TOP_CENTER, "Cat Meds"); // Penelope button (top button) int btn_x = 40; int btn_y = 90; int btn_w = 240; int btn_h = 120; 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 - 20, id(status_font), white, TextAlign::CENTER, "DONE"); } // Tess button (middle button) btn_y = 230; 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 - 20, id(status_font), white, TextAlign::CENTER, "DONE"); } // Reset button (bottom, smaller) int reset_x = 110; int reset_y = 395; int reset_w = 100; int reset_h = 55; 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 id: my_touchscreen spi_id: tft_spi cs_pin: GPIO33 update_interval: 250ms threshold: 1200 calibration: x_min: 280 x_max: 3850 y_min: 340 y_max: 3860 # Touch buttons as binary sensors binary_sensor: - platform: touchscreen touchscreen_id: my_touchscreen name: "Penelope Button" id: penelope_button x_min: 40 x_max: 280 y_min: 90 y_max: 210 on_press: then: - switch.toggle: penelope_medicated - platform: touchscreen touchscreen_id: my_touchscreen name: "Tess Button" id: tess_button x_min: 40 x_max: 280 y_min: 230 y_max: 350 on_press: then: - switch.toggle: tess_medicated - platform: touchscreen touchscreen_id: my_touchscreen name: "Reset Button" id: reset_button x_min: 110 x_max: 210 y_min: 395 y_max: 450 on_press: then: - switch.turn_off: penelope_medicated - switch.turn_off: tess_medicated - 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 # Backlight control output: - platform: gpio pin: GPIO21 id: backlight_pwm inverted: false light: - platform: binary output: backlight_pwm name: "${friendly_name} Backlight" id: backlight restore_mode: ALWAYS_ON # Keep display drawing if boot timing is tight interval: - interval: 2s then: - component.update: my_display # 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 # 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