diff --git a/esphome/cat-medication-tracker.yaml b/esphome/cat-medication-tracker.yaml new file mode 100644 index 0000000..327ee4c --- /dev/null +++ b/esphome/cat-medication-tracker.yaml @@ -0,0 +1,238 @@ +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 wifi_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 + 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 \ No newline at end of file