substitutions: name: cat-medication-tracker friendly_name: "Cat Medication Tracker" esphome: name: ${name} friendly_name: ${friendly_name} includes: - spi_helper.h on_boot: - priority: 100 then: - light.turn_on: backlight - priority: -100 then: - lambda: |- // Send MADCTL 0x88: MY=1, BGR=1 — correct portrait for this board // Uses ESP-IDF SPI master API to bypass ESPHome's buffered display layer spi_device_handle_t disp_fix; spi_device_interface_config_t cfg = {}; cfg.clock_speed_hz = 1000000; cfg.mode = 0; cfg.spics_io_num = -1; // manual CS cfg.queue_size = 1; if (spi_bus_add_device(SPI2_HOST, &cfg, &disp_fix) == ESP_OK) { gpio_set_level((gpio_num_t)15, 0); // CS low gpio_set_level((gpio_num_t)2, 0); // DC = command spi_transaction_t t = {}; t.length = 8; t.flags = SPI_TRANS_USE_TXDATA; t.tx_data[0] = 0x36; // MADCTL register spi_device_polling_transmit(disp_fix, &t); gpio_set_level((gpio_num_t)2, 1); // DC = data t.tx_data[0] = 0x88; // MY=1, BGR=1 spi_device_polling_transmit(disp_fix, &t); gpio_set_level((gpio_num_t)15, 1); // CS high spi_bus_remove_device(disp_fix); } - component.update: my_display esp32: board: esp32dev framework: type: arduino logger: level: DEBUG logs: xpt2046: WARN 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: - platform: homeassistant id: homeassistant_time on_time: - seconds: 0 minutes: 0 hours: 0 then: - switch.turn_off: penelope_medicated - switch.turn_off: tess_medicated - script.execute: update_display spi: - id: tft_spi clk_pin: GPIO14 mosi_pin: GPIO13 miso_pin: GPIO12 display: - platform: mipi_spi model: ILI9488 spi_id: tft_spi cs_pin: GPIO15 dc_pin: GPIO2 reset_pin: GPIO4 rotation: 180 invert_colors: false color_order: bgr data_rate: 10MHz dimensions: width: 320 height: 480 id: my_display auto_clear_enabled: false update_interval: 2s color_depth: 16 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 it.fill(light_grey); // Border: green if all done, red otherwise bool all_done = id(penelope_medicated).state && id(tess_medicated).state; auto border_color = all_done ? green : red; int border = 10; it.filled_rectangle(0, 0, 320, border, border_color); it.filled_rectangle(0, 480 - border, 320, border, border_color); it.filled_rectangle(0, 0, border, 480, border_color); it.filled_rectangle(320 - border, 0, border, 480, border_color); // Title it.printf(160, 30, id(title_font), black, TextAlign::TOP_CENTER, "Cat Meds"); // Penelope 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 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 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: GPIO27 id: backlight_pwm inverted: false light: - platform: binary 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 # 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