initial commit
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
substitutions:
|
||||
device_name: cat-medication-tracker
|
||||
friendly_name: "Cat Medication Tracker"
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: ${friendly_name}
|
||||
includes:
|
||||
- spi_helper.h
|
||||
on_boot:
|
||||
- priority: 100
|
||||
then:
|
||||
- light.turn_on: backlight
|
||||
- priority: -100
|
||||
then:
|
||||
- lambda: |-
|
||||
// MADCTL 0x48: MY=0, MX=1, BGR=1 — correct portrait, no mirror for ESP32-2432S035R
|
||||
// ESP-IDF SPI master API used 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] = 0x48; // MY=0, MX=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: INFO
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
ota:
|
||||
platform: esphome
|
||||
password: !secret ota_password
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_iot_ssid
|
||||
password: !secret wifi_password
|
||||
manual_ip:
|
||||
static_ip: 192.168.69.230
|
||||
gateway: 192.168.69.1
|
||||
subnet: 255.255.255.0
|
||||
ap:
|
||||
ssid: "${device_name}-fallback"
|
||||
password: !secret fallback_password
|
||||
# Prevents aggressive reconnection attempts
|
||||
power_save_mode: none # Use 'light' for battery devices
|
||||
|
||||
# Slower but more reliable connection
|
||||
fast_connect: true
|
||||
# Handle connection failures gracefully
|
||||
on_connect:
|
||||
- logger.log: "Wi-Fi connected!"
|
||||
on_disconnect:
|
||||
- logger.log: "Wi-Fi disconnected!"
|
||||
|
||||
# Reduce mDNS traffic
|
||||
mdns:
|
||||
disabled: true # Set to true if you use static IPs and don't need discovery
|
||||
|
||||
captive_portal:
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
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: 0
|
||||
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
|
||||
transform:
|
||||
mirror_y: false
|
||||
|
||||
# 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: 230
|
||||
y_max: 350
|
||||
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: 90
|
||||
y_max: 210
|
||||
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: 10 #395
|
||||
y_max: 50 #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
|
||||
|
||||
# Diagnostic sensors
|
||||
sensor:
|
||||
- platform: wifi_signal
|
||||
name: "${device_name} WiFi Signal"
|
||||
update_interval: 60s
|
||||
|
||||
- platform: uptime
|
||||
name: "${device_name} Uptime"
|
||||
update_interval: 60s
|
||||
|
||||
text_sensor:
|
||||
- platform: wifi_info
|
||||
ip_address:
|
||||
name: "${device_name} IP Address"
|
||||
ssid:
|
||||
name: "${device_name} Connected SSID"
|
||||
bssid:
|
||||
name: "${device_name} BSSID"
|
||||
|
||||
# Watchdog to auto-reboot if things go wrong
|
||||
interval:
|
||||
- interval: 5min
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- lambda: 'return millis() > 300000;' # >5 min uptime
|
||||
- not:
|
||||
wifi.connected:
|
||||
then:
|
||||
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
|
||||
- delay: 10s
|
||||
- lambda: 'App.safe_reboot();'
|
||||
Reference in New Issue
Block a user