Compare commits
112 Commits
7c347fd524
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1934f3806 | ||
|
|
38b3ab8f56 | ||
|
|
a0e2687bf5 | ||
|
|
c95ad4cd2c | ||
|
|
d32f26da94 | ||
|
|
a4700b673e | ||
|
|
3bd797bcc5 | ||
|
|
1a486240a0 | ||
|
|
ef0841de3f | ||
|
|
6434885ee9 | ||
|
|
72cc7c4524 | ||
|
|
ed17ec0a4d | ||
|
|
a8537e087c | ||
|
|
82ba5d420c | ||
|
|
a2eb2b6106 | ||
|
|
34c92deb26 | ||
|
|
560449b1c3 | ||
|
|
9e2b0b108c | ||
|
|
653d708b78 | ||
|
|
56a378f783 | ||
|
|
812c3fd820 | ||
|
|
cced711c68 | ||
|
|
0569620c6c | ||
|
|
d75297fde4 | ||
|
|
5d761acf69 | ||
|
|
fd0d061f96 | ||
|
|
c684ecba49 | ||
|
|
1b8ec2f489 | ||
|
|
495224d3eb | ||
|
|
eea8f239fa | ||
|
|
6dcab86122 | ||
|
|
7d332586ea | ||
|
|
2220cec970 | ||
|
|
ee85f43a92 | ||
|
|
3a0d7da7f6 | ||
|
|
31aaab69b7 | ||
|
|
a41523a7ac | ||
|
|
71f35fe822 | ||
|
|
6dd6e39c04 | ||
|
|
01e02033f6 | ||
|
|
be876dc24d | ||
|
|
9b86dca27e | ||
|
|
b1bb0deb0e | ||
|
|
f32acd86e4 | ||
|
|
45c9186f25 | ||
|
|
ccb50f1c37 | ||
|
|
0c3f065f64 | ||
|
|
eadb67b2fc | ||
|
|
a9b9559c23 | ||
|
|
487f3f0207 | ||
|
|
7ad2a7b498 | ||
|
|
2681623838 | ||
|
|
57f5745afe | ||
|
|
039c3a901c | ||
|
|
dd0b85137f | ||
|
|
f0c41a6fdc | ||
|
|
58e4ca6c03 | ||
|
|
4aecb61673 | ||
|
|
72f03f9045 | ||
|
|
c43a3767dd | ||
|
|
240fe06229 | ||
|
|
0acf0ab8bf | ||
| d56fa9d393 | |||
| 9cdc4ecba0 | |||
|
|
8aa8ed3273 | ||
|
|
ee07fa4ff7 | ||
|
|
9ccd19cc45 | ||
|
|
8bb8f09f64 | ||
|
|
1937262e8f | ||
|
|
9a91f775c9 | ||
|
|
b976b7f133 | ||
|
|
176a923b67 | ||
|
|
bd845f3f8e | ||
|
|
71ee0ffc4f | ||
|
|
6f451bfd10 | ||
|
|
e416ebe743 | ||
|
|
8521e9bef4 | ||
|
|
7db6976212 | ||
|
|
35b501e978 | ||
|
|
7698a0b79d | ||
|
|
d8f7786cb3 | ||
|
|
4386f37c1b | ||
|
|
d4214ca983 | ||
|
|
4256550c6d | ||
|
|
884cf15451 | ||
|
|
661b40339b | ||
|
|
5053d2662f | ||
|
|
d926c3d9bf | ||
|
|
d6c2b189ef | ||
|
|
338025d55b | ||
|
|
9b4908c39b | ||
|
|
689b173f30 | ||
|
|
f0d6e97cd0 | ||
|
|
7af2d0681c | ||
|
|
b635c771ec | ||
|
|
c2ad832a82 | ||
|
|
eb88e59550 | ||
|
|
c63275d484 | ||
|
|
d7ed0c085c | ||
|
|
3f697d690a | ||
|
|
2474ab72b3 | ||
|
|
23e39c1fe5 | ||
|
|
617cf7d7d1 | ||
|
|
c4ae98cab2 | ||
|
|
b7b78c0785 | ||
|
|
dc815af608 | ||
|
|
21f97a5458 | ||
|
|
d42a23dfab | ||
|
|
c07d58c98b | ||
| 31144a4692 | |||
| 8d6359cdeb | |||
| 705e74d9de |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -30,11 +30,14 @@
|
|||||||
|
|
||||||
# Whitelist specific folders (uncomment as needed)
|
# Whitelist specific folders (uncomment as needed)
|
||||||
!automations/
|
!automations/
|
||||||
!packages/
|
!packages/**
|
||||||
|
!packages/**
|
||||||
|
!packages/chore-tracker-ha.yaml
|
||||||
!scenes/
|
!scenes/
|
||||||
!scripts/
|
!scripts/
|
||||||
!blueprints/
|
!blueprints/
|
||||||
!esphome/
|
!esphome/
|
||||||
|
!esphome/fonts/**
|
||||||
!esphome/ha-remote/
|
!esphome/ha-remote/
|
||||||
!esphome/ha-remote/components/
|
!esphome/ha-remote/components/
|
||||||
!esphome/ha-remote/components/**
|
!esphome/ha-remote/components/**
|
||||||
@@ -47,7 +50,6 @@
|
|||||||
!esphome/chore-tracker/
|
!esphome/chore-tracker/
|
||||||
!esphome/chore-tracker/**
|
!esphome/chore-tracker/**
|
||||||
!esphome/chore-tracker-esphome.yaml
|
!esphome/chore-tracker-esphome.yaml
|
||||||
!chore-tracker-ha.yaml
|
|
||||||
# !packages/
|
# !packages/
|
||||||
# !themes/
|
# !themes/
|
||||||
# !node-red/
|
# !node-red/
|
||||||
|
|||||||
5
esphome/.gitignore
vendored
Normal file
5
esphome/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Gitignore settings for ESPHome
|
||||||
|
# This is an example and may include too much for your use-case.
|
||||||
|
# You can modify this file to suit your needs.
|
||||||
|
/.esphome/
|
||||||
|
/secrets.yaml
|
||||||
293
esphome/cat-medication-tracker.yaml
Normal file
293
esphome/cat-medication-tracker.yaml
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
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: |-
|
||||||
|
// 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: 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: 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
|
||||||
|
|
||||||
|
# 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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -13,30 +13,30 @@ views:
|
|||||||
cards:
|
cards:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
content: >
|
content: >
|
||||||
### 😺 Jordyn
|
### Jordyn
|
||||||
{{
|
{{
|
||||||
'✅ All done!' if states('sensor.jordyn_all_chores_done') == 'True'
|
'All done!' if states('sensor.jordyn_all_chores_done') == 'True'
|
||||||
else states('sensor.jordyn_chores_done_today') ~ '/5 chores done'
|
else states('sensor.jordyn_chores_done_today') ~ '/4 chores done'
|
||||||
}}
|
}}
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
content: >
|
content: >
|
||||||
### 🤓 Declan
|
### Declan
|
||||||
{{
|
{{
|
||||||
'✅ All done!' if states('sensor.declan_all_chores_done') == 'True'
|
'All done!' if states('sensor.declan_all_chores_done') == 'True'
|
||||||
else states('sensor.declan_chores_done_today') ~ '/4 chores done'
|
else states('sensor.declan_chores_done_today') ~ '/2 chores done'
|
||||||
}}
|
}}
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
content: >
|
content: >
|
||||||
### 🌝 Chloe
|
### Chloe
|
||||||
{{
|
{{
|
||||||
'✅ All done!' if states('sensor.chloe_all_chores_done') == 'True'
|
'All done!' if states('sensor.chloe_all_chores_done') == 'True'
|
||||||
else states('sensor.chloe_chores_done_today') ~ '/6 chores done'
|
else states('sensor.chloe_chores_done_today') ~ '/3 chores done'
|
||||||
}}
|
}}
|
||||||
|
|
||||||
- type: button
|
- type: button
|
||||||
name: "↺ Reset ALL Chores"
|
name: "Reset ALL Chores"
|
||||||
icon: mdi:restart-alert
|
icon: mdi:restart-alert
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
@@ -44,87 +44,69 @@ views:
|
|||||||
target:
|
target:
|
||||||
entity_id: input_button.reset_all_chores
|
entity_id: input_button.reset_all_chores
|
||||||
- type: entities
|
- type: entities
|
||||||
title: "😺 Jordyn's Chores"
|
title: " Jordyn's Chores"
|
||||||
entities:
|
entities:
|
||||||
- entity: switch.chore_tracker_jordyn_make_bed
|
- entity: switch.chore_tracker_jordyn_put_on_underwear
|
||||||
name: "🛏️ Make Bed"
|
name: " Put on underwear"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_jordyn_brush_teeth
|
- entity: switch.chore_tracker_jordyn_brush_teeth
|
||||||
name: "🦷 Brush Teeth"
|
name: " Brush Teeth"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_jordyn_tidy_room
|
- entity: switch.chore_tracker_jordyn_fill_water_bowls
|
||||||
name: "🧹 Tidy Room"
|
name: " Fill water bowls"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_jordyn_homework
|
- entity: switch.chore_tracker_jordyn_restock_cat_food_cans
|
||||||
name: "📚 Homework"
|
name: " Restock Cat Food Cans"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_jordyn_feed_dog
|
|
||||||
name: "🐾 Feed Dog"
|
|
||||||
- type: divider
|
- type: divider
|
||||||
- entity: sensor.jordyn_chores_done_today
|
- entity: sensor.jordyn_chores_done_today
|
||||||
name: "Chores done today"
|
name: "Chores done today"
|
||||||
- entity: sensor.jordyn_all_chores_done
|
- entity: sensor.jordyn_all_chores_done
|
||||||
name: "✅ All Done?"
|
name: "All Done?"
|
||||||
- type: button
|
- type: button
|
||||||
name: "↺ Reset Jordyn's Chores"
|
name: "Reset Jordyn's Chores"
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: input_button.press
|
service: input_button.press
|
||||||
target:
|
target:
|
||||||
entity_id: input_button.jordyn_reset_chores
|
entity_id: input_button.jordyn_reset_chores
|
||||||
- type: entities
|
- type: entities
|
||||||
title: "🤓 Declan's Chores"
|
title: " Declan's Chores"
|
||||||
entities:
|
entities:
|
||||||
- entity: switch.chore_tracker_declan_make_bed
|
- entity: switch.chore_tracker_declan_take_morning_pill
|
||||||
name: "🛏️ Make Bed"
|
name: " Take morning pill"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_declan_brush_teeth
|
- entity: switch.chore_tracker_declan_scoop_dog_poop
|
||||||
name: "🦷 Brush Teeth"
|
name: " Scoop Dog Poop"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_declan_set_table
|
|
||||||
name: "🍽️ Set Table"
|
|
||||||
|
|
||||||
- entity: switch.chore_tracker_declan_take_out_trash
|
|
||||||
name: "🗑️ Take Out Trash"
|
|
||||||
- type: divider
|
- type: divider
|
||||||
- entity: sensor.declan_chores_done_today
|
- entity: sensor.declan_chores_done_today
|
||||||
name: "Chores done today"
|
name: "Chores done today"
|
||||||
- entity: sensor.declan_all_chores_done
|
- entity: sensor.declan_all_chores_done
|
||||||
name: "✅ All Done?"
|
name: "All Done?"
|
||||||
- type: button
|
- type: button
|
||||||
name: "↺ Reset Declan's Chores"
|
name: "Reset Declan's Chores"
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: input_button.press
|
service: input_button.press
|
||||||
target:
|
target:
|
||||||
entity_id: input_button.declan_reset_chores
|
entity_id: input_button.declan_reset_chores
|
||||||
- type: entities
|
- type: entities
|
||||||
title: "🌝 Chloe's Chores"
|
title: " Chloe's Chores"
|
||||||
entities:
|
entities:
|
||||||
- entity: switch.chore_tracker_chloe_make_bed
|
- entity: switch.chore_tracker_chloe_fill_kitty_feeders
|
||||||
name: "🛏️ Make Bed"
|
name: " Fill kitty feeders"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_chloe_brush_teeth
|
- entity: switch.chore_tracker_chloe_scoop_kitty_litter
|
||||||
name: "🦷 Brush Teeth"
|
name: " Scoop Kitty Litter"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_chloe_water_plants
|
- entity: switch.chore_tracker_chloe_replace_kitty_litter_bags
|
||||||
name: "🌱 Water Plants"
|
name: " Replace Kitty Litter Bags"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_chloe_homework
|
|
||||||
name: "📚 Homework"
|
|
||||||
|
|
||||||
- entity: switch.chore_tracker_chloe_tidy_room
|
|
||||||
name: "🧹 Tidy Room"
|
|
||||||
|
|
||||||
- entity: switch.chore_tracker_chloe_practice_piano
|
|
||||||
name: "🎹 Practice Piano"
|
|
||||||
- type: divider
|
- type: divider
|
||||||
- entity: sensor.chloe_chores_done_today
|
- entity: sensor.chloe_chores_done_today
|
||||||
name: "Chores done today"
|
name: "Chores done today"
|
||||||
- entity: sensor.chloe_all_chores_done
|
- entity: sensor.chloe_all_chores_done
|
||||||
name: "✅ All Done?"
|
name: "All Done?"
|
||||||
- type: button
|
- type: button
|
||||||
name: "↺ Reset Chloe's Chores"
|
name: "Reset Chloe's Chores"
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: input_button.press
|
service: input_button.press
|
||||||
|
|||||||
@@ -3,12 +3,30 @@
|
|||||||
#
|
#
|
||||||
# Edit this file to change kids, chores, and settings.
|
# Edit this file to change kids, chores, and settings.
|
||||||
# Then run: python3 generate.py
|
# Then run: python3 generate.py
|
||||||
|
#
|
||||||
|
# ICONS: Use MDI codepoints from https://pictogrammers.com/library/mdi/
|
||||||
|
# Format: "\U000FXXXX" e.g. mdi:bed = "\U000F02E3"
|
||||||
|
# The kid avatar field also uses MDI codepoints.
|
||||||
|
#
|
||||||
|
# Common chore icons:
|
||||||
|
# Bed: "\U000F02E3" mdi:bed
|
||||||
|
# Brush teeth: "\U000F09A9" mdi:toothbrush
|
||||||
|
# Broom: "\U000F00A8" mdi:broom
|
||||||
|
# Book: "\U000F0219" mdi:book-open-variant
|
||||||
|
# Dog: "\U000F01F9" mdi:dog
|
||||||
|
# Paw: "\U000F0265" mdi:paw
|
||||||
|
# Silverware: "\U000F03E7" mdi:silverware-fork-knife
|
||||||
|
# Trash: "\U000F05B8" mdi:trash-can
|
||||||
|
# Watering can: "\U000F1B25" mdi:watering-can
|
||||||
|
# Piano: "\U000F0F9E" mdi:piano
|
||||||
|
# Dumbbell: "\U000F0F1E" mdi:dumbbell
|
||||||
|
# Shower: "\U000F0467" mdi:shower
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
device_name: chore-tracker
|
device_name: chore-tracker
|
||||||
friendly_name: "Chore Tracker"
|
friendly_name: "Chore Tracker"
|
||||||
wifi_ssid: !secret wifi_iot_ssid
|
wifi_ssid: !secret wifi_ssid
|
||||||
wifi_password: !secret wifi_password
|
wifi_password: !secret wifi_password
|
||||||
api_key: !secret api_encryption_key
|
api_key: !secret api_encryption_key
|
||||||
ota_password: !secret ota_password
|
ota_password: !secret ota_password
|
||||||
@@ -17,54 +35,42 @@ settings:
|
|||||||
notify_service: notify.notify
|
notify_service: notify.notify
|
||||||
|
|
||||||
# ── KIDS ──────────────────────────────────────────────────────────────────────
|
# ── KIDS ──────────────────────────────────────────────────────────────────────
|
||||||
# Each kid has their own chore list.
|
# avatar: MDI codepoint shown in the sidebar on the kid's chore page
|
||||||
# name, avatar, color, color_dark, and chores are all required.
|
# color / color_dark: hex colours for that kid's sidebar
|
||||||
#
|
#
|
||||||
kids:
|
kids:
|
||||||
- name: Jordyn
|
- name: Jordyn
|
||||||
avatar: "😺"
|
avatar: "\U000F011B" # mdi:cat
|
||||||
color: "4D96FF"
|
color: "5F5980" # #5F5980 is a nice light blue, but it's a bit hard to read when used as a background colour for the sidebar. So I use a darker version of the same colour for the sidebar background.
|
||||||
color_dark: "2A6FCC"
|
color_dark: "2F3061" # #2F3061 is a darker version of #5F5980, and is easier to read when used as a background colour for the sidebar.
|
||||||
chores:
|
chores:
|
||||||
- name: Make Bed
|
- name: Put on underwear
|
||||||
icon: "🛏️"
|
icon: "\U000F02E3" # mdi:bed
|
||||||
- name: Brush Teeth
|
- name: Brush Teeth
|
||||||
icon: "🦷"
|
icon: "\U000F09A9" # mdi:toothbrush
|
||||||
- name: Tidy Room
|
- name: Fill water bowls
|
||||||
icon: "🧹"
|
icon: "\U000F01AB" # mdi:cup-water
|
||||||
- name: Homework
|
- name: Restock Cat Food Cans
|
||||||
icon: "📚"
|
icon: "\U000F171C" # mdi:food-turkey
|
||||||
- name: Feed Dog
|
|
||||||
icon: "🐾"
|
|
||||||
|
|
||||||
- name: Declan
|
- name: Declan
|
||||||
avatar: "🤓"
|
avatar: "\U000F1477" # mdi:wizard-hat
|
||||||
color: "C77DFF"
|
color: "720026" # #720026 is a deep red, used for the kid's sidebar background.
|
||||||
color_dark: "8B42CC"
|
color_dark: "4F000B" # #4F000B is a darker version of #720026, easier to read when used as a background colour for the sidebar.
|
||||||
chores:
|
chores:
|
||||||
- name: Make Bed
|
- name: Take morning pill
|
||||||
icon: "🛏️"
|
icon: "\U000F0402" # mdi:pill
|
||||||
- name: Brush Teeth
|
- name: Scoop Dog Poop
|
||||||
icon: "🦷"
|
icon: "\U000F01F7" # mdi:emoticon-poop
|
||||||
- name: Set Table
|
|
||||||
icon: "🍽️"
|
|
||||||
- name: Take Out Trash
|
|
||||||
icon: "🗑️"
|
|
||||||
|
|
||||||
- name: Chloe
|
- name: Chloe
|
||||||
avatar: "🌝"
|
avatar: "\U000F16A3" # mdi:robot-excited
|
||||||
color: "FF6B9D"
|
color: "73683B" # #73683B is a light brown, used for the kid's sidebar background.
|
||||||
color_dark: "CC3A6F"
|
color_dark: "583E23" # #583E23 is a darker version of #73683B, easier to read when used as a background colour for the sidebar.
|
||||||
chores:
|
chores:
|
||||||
- name: Make Bed
|
- name: Fill kitty feeders
|
||||||
icon: "🛏️"
|
icon: "\U000F02A9" # mdi:bowl-outline
|
||||||
- name: Brush Teeth
|
- name: Scoop Kitty Litter
|
||||||
icon: "🦷"
|
icon: "\U000F01F7" # mdi:emoticon-poop
|
||||||
- name: Water Plants
|
- name: Replace Kitty Litter Bags
|
||||||
icon: "🌱"
|
icon: "\U000F01F7" # mdi:emoticon-poop
|
||||||
- name: Homework
|
|
||||||
icon: "📚"
|
|
||||||
- name: Tidy Room
|
|
||||||
icon: "🧹"
|
|
||||||
- name: Practice Piano
|
|
||||||
icon: "🎹"
|
|
||||||
@@ -7,12 +7,14 @@ Reads chores_config.yaml and generates:
|
|||||||
• chore-tracker-ha.yaml (Home Assistant config)
|
• chore-tracker-ha.yaml (Home Assistant config)
|
||||||
• chore-tracker-dashboard.yaml (Lovelace dashboard)
|
• chore-tracker-dashboard.yaml (Lovelace dashboard)
|
||||||
|
|
||||||
|
Target hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480)
|
||||||
|
|
||||||
Home screen behaviour:
|
Home screen behaviour:
|
||||||
- Each kid's button has a RED outline while any chores remain undone
|
- Each kid's button has a RED outline while any chores remain undone
|
||||||
- Button turns SOLID GREEN when ALL chores are complete
|
- Button turns SOLID GREEN when ALL chores are complete
|
||||||
- Resets to red automatically at midnight
|
- Resets to red automatically at midnight
|
||||||
|
|
||||||
No star tracking — chores are simply done or not done.
|
Each kid must define their own chores list in chores_config.yaml.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 generate.py
|
python3 generate.py
|
||||||
@@ -35,6 +37,10 @@ except ImportError:
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Screen padding — space kept clear around all edges (pixels)
|
||||||
|
PAD = 12
|
||||||
|
|
||||||
|
|
||||||
def slug(name: str) -> str:
|
def slug(name: str) -> str:
|
||||||
return name.lower().replace(" ", "_").replace("-", "_")
|
return name.lower().replace(" ", "_").replace("-", "_")
|
||||||
|
|
||||||
@@ -47,9 +53,15 @@ def eid(kid: dict, chore: dict) -> str:
|
|||||||
def ha_switch(kid: dict, chore: dict) -> str:
|
def ha_switch(kid: dict, chore: dict) -> str:
|
||||||
return f"switch.chore_tracker_{eid(kid, chore)}"
|
return f"switch.chore_tracker_{eid(kid, chore)}"
|
||||||
|
|
||||||
|
# mdi:checkbox-marked-circle-outline — safe default if icon is omitted
|
||||||
|
DEFAULT_ICON = "\U000F0133"
|
||||||
|
|
||||||
def get_chores(kid: dict) -> list:
|
def get_chores(kid: dict) -> list:
|
||||||
"""Each kid must define their own chore list."""
|
chores = kid["chores"]
|
||||||
return kid["chores"]
|
for c in chores:
|
||||||
|
if "icon" not in c:
|
||||||
|
c["icon"] = DEFAULT_ICON
|
||||||
|
return chores
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -60,12 +72,12 @@ def gen_esphome(cfg: dict) -> str:
|
|||||||
s = cfg["settings"]
|
s = cfg["settings"]
|
||||||
kids = cfg["kids"]
|
kids = cfg["kids"]
|
||||||
|
|
||||||
# ── Switches ──────────────────────────────────────────────────────────────
|
# ── Switches (one per chore per kid) ──────────────────────────────────────
|
||||||
switch_blocks = []
|
switch_blocks = []
|
||||||
for kid in kids:
|
for kid in kids:
|
||||||
chores = get_chores(kid)
|
chores = get_chores(kid)
|
||||||
ks = kid_slug(kid)
|
ks = kid_slug(kid)
|
||||||
switch_blocks.append(f"\n # ── {kid['name']}'s chores ──────────────────────────")
|
switch_blocks.append(f"\n # ── {kid['name']}'s chores ──────────────────────────\n")
|
||||||
for chore in chores:
|
for chore in chores:
|
||||||
e = eid(kid, chore)
|
e = eid(kid, chore)
|
||||||
switch_blocks.append(f"""\
|
switch_blocks.append(f"""\
|
||||||
@@ -91,7 +103,7 @@ def gen_esphome(cfg: dict) -> str:
|
|||||||
entity_id: switch.chore_tracker_{e}
|
entity_id: switch.chore_tracker_{e}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# ── Sensors (chores-done count only) ──────────────────────────────────────
|
# ── Sensors (chores-done count only — no stars) ────────────────────────
|
||||||
sensor_blocks = []
|
sensor_blocks = []
|
||||||
for kid in kids:
|
for kid in kids:
|
||||||
chores = get_chores(kid)
|
chores = get_chores(kid)
|
||||||
@@ -112,7 +124,7 @@ def gen_esphome(cfg: dict) -> str:
|
|||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# ── Midnight reset ────────────────────────────────────────────────────────
|
# ── Midnight reset calls ──────────────────────────────────────────────────
|
||||||
reset_calls = "\n".join(
|
reset_calls = "\n".join(
|
||||||
f" id(reset_{kid_slug(k)}_chores).execute();"
|
f" id(reset_{kid_slug(k)}_chores).execute();"
|
||||||
for k in kids
|
for k in kids
|
||||||
@@ -121,6 +133,13 @@ def gen_esphome(cfg: dict) -> str:
|
|||||||
lvgl_pages = _gen_lvgl_pages(kids)
|
lvgl_pages = _gen_lvgl_pages(kids)
|
||||||
scripts = _gen_scripts(kids)
|
scripts = _gen_scripts(kids)
|
||||||
|
|
||||||
|
# Collect all unique MDI glyphs used (avatars + chore icons)
|
||||||
|
all_glyphs = sorted(set(
|
||||||
|
[k["avatar"] for k in kids] +
|
||||||
|
[c["icon"] for k in kids for c in get_chores(k)]
|
||||||
|
))
|
||||||
|
glyphs_str = "".join(all_glyphs)
|
||||||
|
|
||||||
return f"""\
|
return f"""\
|
||||||
################################################################################
|
################################################################################
|
||||||
# chore-tracker-esphome.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
# chore-tracker-esphome.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
||||||
@@ -128,19 +147,30 @@ def gen_esphome(cfg: dict) -> str:
|
|||||||
#
|
#
|
||||||
# HOME SCREEN BEHAVIOUR:
|
# HOME SCREEN BEHAVIOUR:
|
||||||
# Red outline = chores incomplete
|
# Red outline = chores incomplete
|
||||||
# Solid green = all chores done ✓
|
# Solid green = all chores done
|
||||||
# Resets to red automatically at midnight
|
# Resets to red automatically at midnight
|
||||||
|
#
|
||||||
|
# Hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480)
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: {s["device_name"]}
|
name: {s["device_name"]}
|
||||||
friendly_name: "{s["friendly_name"]}"
|
friendly_name: "{s["friendly_name"]}"
|
||||||
|
on_boot:
|
||||||
|
priority: -10
|
||||||
|
then:
|
||||||
|
- light.turn_on: backlight
|
||||||
|
- lvgl.page.show: page_home
|
||||||
|
|
||||||
esp32:
|
esp32:
|
||||||
board: esp32s3box
|
board: esp32-s3-devkitc-1
|
||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
type: esp-idf
|
||||||
|
|
||||||
|
psram:
|
||||||
|
mode: octal
|
||||||
|
speed: 80MHz
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: {s["wifi_ssid"]}
|
ssid: {s["wifi_ssid"]}
|
||||||
password: {s["wifi_password"]}
|
password: {s["wifi_password"]}
|
||||||
@@ -160,54 +190,58 @@ ota:
|
|||||||
- platform: esphome
|
- platform: esphome
|
||||||
password: {s["ota_password"]}
|
password: {s["ota_password"]}
|
||||||
|
|
||||||
# ── Display — Waveshare ESP32-S3 7" (adjust pins for your board revision) ─────
|
# ── I2C ───────────────────────────────────────────────────────────────────────
|
||||||
display:
|
|
||||||
- platform: rpi_dpi_rgb
|
|
||||||
id: main_display
|
|
||||||
auto_clear_enabled: false
|
|
||||||
color_order: RGB
|
|
||||||
dimensions:
|
|
||||||
width: 800
|
|
||||||
height: 480
|
|
||||||
de_pin:
|
|
||||||
number: GPIO40
|
|
||||||
ignore_strapping_warning: true
|
|
||||||
hsync_pin:
|
|
||||||
number: GPIO39
|
|
||||||
ignore_strapping_warning: true
|
|
||||||
vsync_pin:
|
|
||||||
number: GPIO41
|
|
||||||
pclk_pin: GPIO42
|
|
||||||
data_pins:
|
|
||||||
red: [GPIO45, GPIO48, GPIO47, GPIO21, GPIO14]
|
|
||||||
green: [GPIO5, GPIO6, GPIO7, GPIO15, GPIO16, GPIO4]
|
|
||||||
blue: [GPIO8, GPIO3, GPIO46, GPIO9, GPIO1]
|
|
||||||
|
|
||||||
touchscreen:
|
|
||||||
- platform: gt911
|
|
||||||
id: touch
|
|
||||||
display: main_display
|
|
||||||
i2c_id: i2c_touch
|
|
||||||
interrupt_pin: GPIO2
|
|
||||||
reset_pin: GPIO38
|
|
||||||
|
|
||||||
i2c:
|
i2c:
|
||||||
- id: i2c_touch
|
sda: 8
|
||||||
sda: GPIO19
|
scl: 9
|
||||||
scl: GPIO20
|
|
||||||
frequency: 400kHz
|
|
||||||
|
|
||||||
|
# ── CH422G IO Expander (controls LCD reset, touch reset, backlight) ───────────
|
||||||
|
ch422g:
|
||||||
|
- id: ch422g_hub
|
||||||
|
|
||||||
|
# ── Display ───────────────────────────────────────────────────────────────────
|
||||||
|
display:
|
||||||
|
- platform: mipi_rgb
|
||||||
|
model: ESP32-S3-TOUCH-LCD-7-800X480
|
||||||
|
id: main_display
|
||||||
|
update_interval: never
|
||||||
|
auto_clear_enabled: false
|
||||||
|
reset_pin:
|
||||||
|
ch422g: ch422g_hub
|
||||||
|
number: 3
|
||||||
|
mode:
|
||||||
|
output: true
|
||||||
|
|
||||||
|
# ── Touchscreen ───────────────────────────────────────────────────────────────
|
||||||
|
touchscreen:
|
||||||
|
platform: gt911
|
||||||
|
id: touch
|
||||||
|
update_interval: 120ms
|
||||||
|
reset_pin:
|
||||||
|
ch422g: ch422g_hub
|
||||||
|
number: 1
|
||||||
|
mode:
|
||||||
|
output: true
|
||||||
|
|
||||||
|
# ── Backlight ─────────────────────────────────────────────────────────────────
|
||||||
output:
|
output:
|
||||||
- platform: ledc
|
- platform: template
|
||||||
pin: GPIO17
|
id: lcd_backlight_out
|
||||||
id: backlight_output
|
type: binary
|
||||||
|
write_action:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return state;
|
||||||
|
then:
|
||||||
|
- switch.turn_on: lcd_backlight_raw
|
||||||
|
else:
|
||||||
|
- switch.turn_off: lcd_backlight_raw
|
||||||
|
|
||||||
light:
|
light:
|
||||||
- platform: monochromatic
|
- platform: binary
|
||||||
output: backlight_output
|
|
||||||
name: "Display Backlight"
|
name: "Display Backlight"
|
||||||
|
output: lcd_backlight_out
|
||||||
id: backlight
|
id: backlight
|
||||||
restore_mode: ALWAYS_ON
|
|
||||||
|
|
||||||
# ── Midnight reset ────────────────────────────────────────────────────────────
|
# ── Midnight reset ────────────────────────────────────────────────────────────
|
||||||
time:
|
time:
|
||||||
@@ -222,25 +256,46 @@ time:
|
|||||||
{reset_calls}
|
{reset_calls}
|
||||||
- lvgl.page.show: page_home
|
- lvgl.page.show: page_home
|
||||||
|
|
||||||
fonts:
|
# ── Fonts (place files in /config/esphome/fonts/) ────────────────────────────
|
||||||
- file: "gfonts://Nunito:wght@900"
|
font:
|
||||||
|
- file: "fonts/Nunito-Black.ttf"
|
||||||
id: font_title
|
id: font_title
|
||||||
size: 40
|
size: 40
|
||||||
- file: "gfonts://Nunito:wght@800"
|
- file: "fonts/Nunito-ExtraBold.ttf"
|
||||||
id: font_name
|
id: font_name
|
||||||
size: 30
|
size: 30
|
||||||
- file: "gfonts://Nunito:wght@700"
|
- file: "fonts/Nunito-Bold.ttf"
|
||||||
id: font_med
|
id: font_med
|
||||||
size: 22
|
size: 22
|
||||||
- file: "gfonts://Nunito:wght@600"
|
- file: "fonts/Nunito-SemiBold.ttf"
|
||||||
id: font_small
|
id: font_small
|
||||||
size: 17
|
size: 17
|
||||||
- file: "gfonts://Nunito:wght@600"
|
- file: "fonts/Nunito-SemiBold.ttf"
|
||||||
id: font_tiny
|
id: font_tiny
|
||||||
size: 13
|
size: 13
|
||||||
|
# MDI icon font — used for chore icons and kid avatars
|
||||||
|
- file: "fonts/materialdesignicons-webfont.ttf"
|
||||||
|
id: font_mdi_large
|
||||||
|
size: 48
|
||||||
|
bpp: 4
|
||||||
|
glyphs: {glyphs_str}
|
||||||
|
- file: "fonts/materialdesignicons-webfont.ttf"
|
||||||
|
id: font_mdi_small
|
||||||
|
size: 32
|
||||||
|
bpp: 4
|
||||||
|
glyphs: {glyphs_str}
|
||||||
|
|
||||||
# ── Switches — one per chore per kid, synced to HA ───────────────────────────
|
# ── Switches — backlight raw + one per chore per kid ─────────────────────────
|
||||||
switch:
|
switch:
|
||||||
|
- platform: gpio
|
||||||
|
id: lcd_backlight_raw
|
||||||
|
name: "LCD Backlight Raw"
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
pin:
|
||||||
|
ch422g: ch422g_hub
|
||||||
|
number: 2
|
||||||
|
mode:
|
||||||
|
output: true
|
||||||
{"".join(switch_blocks)}
|
{"".join(switch_blocks)}
|
||||||
|
|
||||||
# ── Sensors — reported to HA ─────────────────────────────────────────────────
|
# ── Sensors — reported to HA ─────────────────────────────────────────────────
|
||||||
@@ -258,7 +313,7 @@ lvgl:
|
|||||||
touchscreens:
|
touchscreens:
|
||||||
- touch
|
- touch
|
||||||
theme:
|
theme:
|
||||||
btn:
|
button:
|
||||||
radius: 20
|
radius: 20
|
||||||
border_width: 0
|
border_width: 0
|
||||||
pages:
|
pages:
|
||||||
@@ -275,10 +330,11 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
|
|
||||||
# ── HOME PAGE ─────────────────────────────────────────────────────────────
|
# ── HOME PAGE ─────────────────────────────────────────────────────────────
|
||||||
n = len(kids)
|
n = len(kids)
|
||||||
btn_w = min(200, max(140, (760 - 20 * (n - 1)) // n))
|
usable_w = 800 - PAD * 2
|
||||||
|
btn_w = min(200, max(140, (usable_w - 40 - 20 * (n - 1)) // n))
|
||||||
total_w = btn_w * n + 20 * (n - 1)
|
total_w = btn_w * n + 20 * (n - 1)
|
||||||
start_x = (800 - total_w) // 2
|
start_x = PAD + (usable_w - total_w) // 2
|
||||||
btn_y = 130
|
btn_y = PAD + 118
|
||||||
btn_h = 210
|
btn_h = 210
|
||||||
|
|
||||||
reset_all_calls = "\n".join(
|
reset_all_calls = "\n".join(
|
||||||
@@ -291,16 +347,15 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
x = start_x + i * (btn_w + 20)
|
x = start_x + i * (btn_w + 20)
|
||||||
ks = kid_slug(kid)
|
ks = kid_slug(kid)
|
||||||
home_btns += f"""\
|
home_btns += f"""\
|
||||||
- btn:
|
- button:
|
||||||
id: home_btn_{ks}
|
id: home_btn_{ks}
|
||||||
x: {x}
|
x: {x}
|
||||||
y: {btn_y}
|
y: {btn_y}
|
||||||
width: {btn_w}
|
width: {btn_w}
|
||||||
height: {btn_h}
|
height: {btn_h}
|
||||||
bg_color: 0xFFFFFF
|
bg_color: 0xFF4757
|
||||||
bg_opa: TRANSP
|
bg_opa: COVER
|
||||||
border_color: 0xFF4757
|
border_width: 0
|
||||||
border_width: 5
|
|
||||||
radius: 24
|
radius: 24
|
||||||
on_click:
|
on_click:
|
||||||
then:
|
then:
|
||||||
@@ -310,20 +365,20 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
align: CENTER
|
align: CENTER
|
||||||
y: -45
|
y: -45
|
||||||
text: "{kid['avatar']}"
|
text: "{kid['avatar']}"
|
||||||
text_font: font_title
|
text_font: font_mdi_large
|
||||||
- label:
|
- label:
|
||||||
align: CENTER
|
align: CENTER
|
||||||
y: 22
|
y: 22
|
||||||
text: "{kid['name']}"
|
text: "{kid['name']}"
|
||||||
text_font: font_name
|
text_font: font_name
|
||||||
text_color: 0x2D3436
|
text_color: 0xFFFFFF
|
||||||
- label:
|
- label:
|
||||||
id: home_status_{ks}
|
id: home_status_{ks}
|
||||||
align: CENTER
|
align: CENTER
|
||||||
y: 66
|
y: 66
|
||||||
text: "not done"
|
text: "not done"
|
||||||
text_font: font_tiny
|
text_font: font_tiny
|
||||||
text_color: 0xFF4757
|
text_color: 0xFFFFFF
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -334,7 +389,7 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
- label:
|
- label:
|
||||||
x: 0
|
x: 0
|
||||||
y: 36
|
y: 36
|
||||||
width: 800
|
width: {800 - PAD * 2}
|
||||||
align: TOP_MID
|
align: TOP_MID
|
||||||
text: "Chore Tracker"
|
text: "Chore Tracker"
|
||||||
text_font: font_title
|
text_font: font_title
|
||||||
@@ -342,14 +397,14 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
- label:
|
- label:
|
||||||
x: 0
|
x: 0
|
||||||
y: 90
|
y: 90
|
||||||
width: 800
|
width: {800 - PAD * 2}
|
||||||
align: TOP_MID
|
align: TOP_MID
|
||||||
text: "Tap a name to check off chores"
|
text: "Tap a name to check off chores"
|
||||||
text_font: font_small
|
text_font: font_small
|
||||||
text_color: 0xB2BEC3
|
text_color: 0xB2BEC3
|
||||||
- btn:
|
- button:
|
||||||
x: 640
|
x: 640
|
||||||
y: 424
|
y: 428
|
||||||
width: 148
|
width: 148
|
||||||
height: 40
|
height: 40
|
||||||
bg_color: 0xFF4757
|
bg_color: 0xFF4757
|
||||||
@@ -360,7 +415,7 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
widgets:
|
widgets:
|
||||||
- label:
|
- label:
|
||||||
align: CENTER
|
align: CENTER
|
||||||
text: "↺ Reset All"
|
text: "Reset All"
|
||||||
text_color: 0xFFFFFF
|
text_color: 0xFFFFFF
|
||||||
text_font: font_tiny
|
text_font: font_tiny
|
||||||
{home_btns}""")
|
{home_btns}""")
|
||||||
@@ -375,21 +430,21 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
rows = (n_chores + cols - 1) // cols
|
rows = (n_chores + cols - 1) // cols
|
||||||
sidebar_w = 172
|
sidebar_w = 172
|
||||||
gap = 10
|
gap = 10
|
||||||
content_w = 800 - sidebar_w - gap * 2
|
content_w = 800 - sidebar_w - gap - 12 # right pad
|
||||||
card_w = (content_w - gap * (cols - 1)) // cols
|
card_w = (content_w - gap * (cols - 1)) // cols
|
||||||
card_h = min(138, (480 - gap * (rows + 1)) // rows)
|
card_h = min(138, (480 - PAD - gap * (rows + 1)) // rows)
|
||||||
|
|
||||||
card_widgets = ""
|
card_widgets = ""
|
||||||
for idx, chore in enumerate(chores):
|
for idx, chore in enumerate(chores):
|
||||||
row = idx // cols
|
row = idx // cols
|
||||||
col = idx % cols
|
col = idx % cols
|
||||||
x = sidebar_w + gap + col * (card_w + gap)
|
x = sidebar_w + gap + col * (card_w + gap)
|
||||||
y = gap + row * (card_h + gap)
|
y = 12 + row * (card_h + gap)
|
||||||
e = eid(kid, chore)
|
e = eid(kid, chore)
|
||||||
icon_y = -(card_h // 4)
|
icon_y = -(card_h // 4)
|
||||||
label_y = card_h // 8
|
label_y = card_h // 8
|
||||||
card_widgets += f"""\
|
card_widgets += f"""\
|
||||||
- btn:
|
- button:
|
||||||
id: card_{e}
|
id: card_{e}
|
||||||
x: {x}
|
x: {x}
|
||||||
y: {y}
|
y: {y}
|
||||||
@@ -417,7 +472,8 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
align: CENTER
|
align: CENTER
|
||||||
y: {icon_y}
|
y: {icon_y}
|
||||||
text: "{chore['icon']}"
|
text: "{chore['icon']}"
|
||||||
text_font: font_med
|
text_font: font_mdi_small
|
||||||
|
text_color: 0x{kid['color']}
|
||||||
- label:
|
- label:
|
||||||
align: CENTER
|
align: CENTER
|
||||||
y: {label_y}
|
y: {label_y}
|
||||||
@@ -449,7 +505,7 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
width: {sidebar_w}
|
width: {sidebar_w}
|
||||||
align: TOP_MID
|
align: TOP_MID
|
||||||
text: "{kid['avatar']}"
|
text: "{kid['avatar']}"
|
||||||
text_font: font_title
|
text_font: font_mdi_large
|
||||||
- label:
|
- label:
|
||||||
x: 0
|
x: 0
|
||||||
y: 76
|
y: 76
|
||||||
@@ -488,9 +544,9 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
text: ""
|
text: ""
|
||||||
text_font: font_small
|
text_font: font_small
|
||||||
text_color: 0xFFFFFF
|
text_color: 0xFFFFFF
|
||||||
- btn:
|
- button:
|
||||||
x: 14
|
x: 14
|
||||||
y: 364
|
y: {480 - PAD - 44 - 6 - 44}
|
||||||
width: {sidebar_w - 28}
|
width: {sidebar_w - 28}
|
||||||
height: 44
|
height: 44
|
||||||
bg_color: 0xFF4757
|
bg_color: 0xFF4757
|
||||||
@@ -501,12 +557,12 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
widgets:
|
widgets:
|
||||||
- label:
|
- label:
|
||||||
align: CENTER
|
align: CENTER
|
||||||
text: "↺ Reset"
|
text: "Reset"
|
||||||
text_color: 0xFFFFFF
|
text_color: 0xFFFFFF
|
||||||
text_font: font_small
|
text_font: font_small
|
||||||
- btn:
|
- button:
|
||||||
x: 14
|
x: 14
|
||||||
y: 420
|
y: {480 - PAD - 44}
|
||||||
width: {sidebar_w - 28}
|
width: {sidebar_w - 28}
|
||||||
height: 44
|
height: 44
|
||||||
bg_color: 0x{kid['color_dark']}
|
bg_color: 0x{kid['color_dark']}
|
||||||
@@ -517,7 +573,7 @@ def _gen_lvgl_pages(kids: list) -> str:
|
|||||||
widgets:
|
widgets:
|
||||||
- label:
|
- label:
|
||||||
align: CENTER
|
align: CENTER
|
||||||
text: "◀ Home"
|
text: "Home"
|
||||||
text_color: 0xFFFFFF
|
text_color: 0xFFFFFF
|
||||||
text_font: font_small
|
text_font: font_small
|
||||||
|
|
||||||
@@ -560,7 +616,6 @@ def _gen_scripts(kids: list) -> str:
|
|||||||
f" id({eid(kid,c)}).turn_off();" for c in chores
|
f" id({eid(kid,c)}).turn_off();" for c in chores
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reset script
|
|
||||||
blocks.append(f"""\
|
blocks.append(f"""\
|
||||||
- id: reset_{ks}_chores
|
- id: reset_{ks}_chores
|
||||||
mode: single
|
mode: single
|
||||||
@@ -571,7 +626,6 @@ def _gen_scripts(kids: list) -> str:
|
|||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# UI update script
|
|
||||||
blocks.append(f"""\
|
blocks.append(f"""\
|
||||||
- id: update_{ks}_ui
|
- id: update_{ks}_ui
|
||||||
mode: single
|
mode: single
|
||||||
@@ -587,9 +641,9 @@ def _gen_scripts(kids: list) -> str:
|
|||||||
lv_label_set_text(id(progress_label_{ks}), buf);
|
lv_label_set_text(id(progress_label_{ks}), buf);
|
||||||
|
|
||||||
// All-done message in sidebar
|
// All-done message in sidebar
|
||||||
lv_label_set_text(id(all_done_label_{ks}), done == total ? "\\U0001F389 All done!" : "");
|
lv_label_set_text(id(all_done_label_{ks}), done == total ? "All done!" : "");
|
||||||
|
|
||||||
// Home button colour
|
// Home button: RED outline = incomplete, SOLID GREEN = all done
|
||||||
if (done == total) {{
|
if (done == total) {{
|
||||||
lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_COVER, LV_PART_MAIN);
|
lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_COVER, LV_PART_MAIN);
|
||||||
lv_obj_set_style_bg_color(id(home_btn_{ks}), lv_color_hex(0x6BCB77), LV_PART_MAIN);
|
lv_obj_set_style_bg_color(id(home_btn_{ks}), lv_color_hex(0x6BCB77), LV_PART_MAIN);
|
||||||
@@ -597,9 +651,9 @@ def _gen_scripts(kids: list) -> str:
|
|||||||
lv_label_set_text(id(home_status_{ks}), "\\u2713 all done!");
|
lv_label_set_text(id(home_status_{ks}), "\\u2713 all done!");
|
||||||
lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFFFFFF), LV_PART_MAIN);
|
lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFFFFFF), LV_PART_MAIN);
|
||||||
}} else {{
|
}} else {{
|
||||||
lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_TRANSP, LV_PART_MAIN);
|
lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_COVER, LV_PART_MAIN);
|
||||||
lv_obj_set_style_border_color(id(home_btn_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN);
|
lv_obj_set_style_bg_color(id(home_btn_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN);
|
||||||
lv_obj_set_style_border_width(id(home_btn_{ks}), 5, LV_PART_MAIN);
|
lv_obj_set_style_border_width(id(home_btn_{ks}), 0, LV_PART_MAIN);
|
||||||
snprintf(buf, sizeof(buf), "%d left", total - done);
|
snprintf(buf, sizeof(buf), "%d left", total - done);
|
||||||
lv_label_set_text(id(home_status_{ks}), buf);
|
lv_label_set_text(id(home_status_{ks}), buf);
|
||||||
lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN);
|
lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN);
|
||||||
@@ -643,7 +697,6 @@ def gen_ha(cfg: dict) -> str:
|
|||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# input_button helpers
|
|
||||||
input_buttons = "".join(f"""\
|
input_buttons = "".join(f"""\
|
||||||
{kid_slug(k)}_reset_chores:
|
{kid_slug(k)}_reset_chores:
|
||||||
name: "Reset {k['name']}'s Chores"
|
name: "Reset {k['name']}'s Chores"
|
||||||
@@ -655,10 +708,8 @@ def gen_ha(cfg: dict) -> str:
|
|||||||
icon: mdi:restart-alert
|
icon: mdi:restart-alert
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Automations
|
|
||||||
auto_blocks = []
|
auto_blocks = []
|
||||||
|
|
||||||
# Per-kid HA reset button
|
|
||||||
for kid in kids:
|
for kid in kids:
|
||||||
ks = kid_slug(kid)
|
ks = kid_slug(kid)
|
||||||
off_calls = "\n".join(
|
off_calls = "\n".join(
|
||||||
@@ -667,7 +718,7 @@ def gen_ha(cfg: dict) -> str:
|
|||||||
)
|
)
|
||||||
auto_blocks.append(f"""\
|
auto_blocks.append(f"""\
|
||||||
- id: {ks}_reset_from_ha
|
- id: {ks}_reset_from_ha
|
||||||
alias: "Chore Tracker — Reset {kid['name']}'s chores from HA"
|
alias: "Chore Tracker - Reset {kid['name']}'s chores from HA"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: input_button.{ks}_reset_chores
|
entity_id: input_button.{ks}_reset_chores
|
||||||
@@ -676,29 +727,27 @@ def gen_ha(cfg: dict) -> str:
|
|||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Reset All from HA
|
all_off = "\n".join(
|
||||||
all_off_calls = "\n".join(
|
|
||||||
f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}"
|
f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}"
|
||||||
for kid in kids for c in get_chores(kid)
|
for kid in kids for c in get_chores(kid)
|
||||||
)
|
)
|
||||||
auto_blocks.append(f"""\
|
auto_blocks.append(f"""\
|
||||||
- id: reset_all_from_ha
|
- id: reset_all_from_ha
|
||||||
alias: "Chore Tracker — Reset ALL chores from HA"
|
alias: "Chore Tracker - Reset ALL chores from HA"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: input_button.reset_all_chores
|
entity_id: input_button.reset_all_chores
|
||||||
action:
|
action:
|
||||||
{all_off_calls}
|
{all_off}
|
||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# All-done notification
|
|
||||||
for kid in kids:
|
for kid in kids:
|
||||||
ks = kid_slug(kid)
|
ks = kid_slug(kid)
|
||||||
n = len(get_chores(kid))
|
n = len(get_chores(kid))
|
||||||
auto_blocks.append(f"""\
|
auto_blocks.append(f"""\
|
||||||
- id: {ks}_all_done_notify
|
- id: {ks}_all_done_notify
|
||||||
alias: "Chore Tracker — {kid['name']} all done!"
|
alias: "Chore Tracker - {kid['name']} all done!"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: sensor.{ks}_all_chores_done
|
entity_id: sensor.{ks}_all_chores_done
|
||||||
@@ -706,12 +755,11 @@ def gen_ha(cfg: dict) -> str:
|
|||||||
action:
|
action:
|
||||||
- service: {s['notify_service']}
|
- service: {s['notify_service']}
|
||||||
data:
|
data:
|
||||||
title: "🎉 {kid['name']} finished all chores!"
|
title: "{kid['name']} finished all chores!"
|
||||||
message: "{kid['name']} completed all {n} chores today!"
|
message: "{kid['name']} completed all {n} chores today!"
|
||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Evening reminder
|
|
||||||
reminder_actions = "".join(f"""\
|
reminder_actions = "".join(f"""\
|
||||||
- if:
|
- if:
|
||||||
condition: template
|
condition: template
|
||||||
@@ -719,7 +767,7 @@ def gen_ha(cfg: dict) -> str:
|
|||||||
then:
|
then:
|
||||||
- service: {s['notify_service']}
|
- service: {s['notify_service']}
|
||||||
data:
|
data:
|
||||||
title: "📋 {k['name']} has unfinished chores"
|
title: "{k['name']} has unfinished chores"
|
||||||
message: >
|
message: >
|
||||||
{k['name']} has done
|
{k['name']} has done
|
||||||
{{{{ states('sensor.{kid_slug(k)}_chores_done_today') }}}}/{len(get_chores(k))} chores today.
|
{{{{ states('sensor.{kid_slug(k)}_chores_done_today') }}}}/{len(get_chores(k))} chores today.
|
||||||
@@ -727,7 +775,7 @@ def gen_ha(cfg: dict) -> str:
|
|||||||
|
|
||||||
auto_blocks.append(f"""\
|
auto_blocks.append(f"""\
|
||||||
- id: chore_reminder_evening
|
- id: chore_reminder_evening
|
||||||
alias: "Chore Tracker — Evening reminder"
|
alias: "Chore Tracker - Evening reminder"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: time
|
- platform: time
|
||||||
at: "{s['reminder_time']}:00"
|
at: "{s['reminder_time']}:00"
|
||||||
@@ -741,8 +789,8 @@ def gen_ha(cfg: dict) -> str:
|
|||||||
# Kids: {", ".join(k["name"] for k in kids)}
|
# Kids: {", ".join(k["name"] for k in kids)}
|
||||||
#
|
#
|
||||||
# BIDIRECTIONAL SYNC:
|
# BIDIRECTIONAL SYNC:
|
||||||
# Screen → HA: Each switch calls homeassistant.service on toggle
|
# Screen to HA: Each switch calls homeassistant.service on toggle
|
||||||
# HA → Screen: ESPHome native API handles this automatically
|
# HA to Screen: ESPHome native API handles this automatically
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
input_button:
|
input_button:
|
||||||
@@ -767,7 +815,7 @@ def gen_dashboard(cfg: dict) -> str:
|
|||||||
content: >
|
content: >
|
||||||
### {kid['avatar']} {kid['name']}
|
### {kid['avatar']} {kid['name']}
|
||||||
{{{{
|
{{{{
|
||||||
'✅ All done!' if states('sensor.{kid_slug(kid)}_all_chores_done') == 'True'
|
'All done!' if states('sensor.{kid_slug(kid)}_all_chores_done') == 'True'
|
||||||
else states('sensor.{kid_slug(kid)}_chores_done_today') ~ '/{len(get_chores(kid))} chores done'
|
else states('sensor.{kid_slug(kid)}_chores_done_today') ~ '/{len(get_chores(kid))} chores done'
|
||||||
}}}}
|
}}}}
|
||||||
""" for kid in kids)
|
""" for kid in kids)
|
||||||
@@ -789,9 +837,9 @@ def gen_dashboard(cfg: dict) -> str:
|
|||||||
- entity: sensor.{ks}_chores_done_today
|
- entity: sensor.{ks}_chores_done_today
|
||||||
name: "Chores done today"
|
name: "Chores done today"
|
||||||
- entity: sensor.{ks}_all_chores_done
|
- entity: sensor.{ks}_all_chores_done
|
||||||
name: "✅ All Done?"
|
name: "All Done?"
|
||||||
- type: button
|
- type: button
|
||||||
name: "↺ Reset {kid['name']}'s Chores"
|
name: "Reset {kid['name']}'s Chores"
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: input_button.press
|
service: input_button.press
|
||||||
@@ -815,7 +863,7 @@ views:
|
|||||||
cards:
|
cards:
|
||||||
{summary_cards}
|
{summary_cards}
|
||||||
- type: button
|
- type: button
|
||||||
name: "↺ Reset ALL Chores"
|
name: "Reset ALL Chores"
|
||||||
icon: mdi:restart-alert
|
icon: mdi:restart-alert
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
@@ -859,18 +907,24 @@ def main():
|
|||||||
print("❌ No kids defined in config!")
|
print("❌ No kids defined in config!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Validate every kid has their own chore list
|
|
||||||
errors = [k["name"] for k in kids if "chores" not in k or not k["chores"]]
|
errors = [k["name"] for k in kids if "chores" not in k or not k["chores"]]
|
||||||
if errors:
|
if errors:
|
||||||
print(f"❌ These kids have no chores defined: {', '.join(errors)}")
|
print(f"❌ These kids have no chores defined: {', '.join(errors)}")
|
||||||
print(" Add a 'chores:' list under each kid in chores_config.yaml")
|
print(" Add a 'chores:' list under each kid in chores_config.yaml")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Warn about missing icons (will use default, not crash)
|
||||||
|
for k in kids:
|
||||||
|
for c in k.get("chores", []):
|
||||||
|
if "icon" not in c:
|
||||||
|
print(f"⚠️ {k['name']} / '{c['name']}' has no icon — using default")
|
||||||
|
|
||||||
total_chores = sum(len(get_chores(k)) for k in kids)
|
total_chores = sum(len(get_chores(k)) for k in kids)
|
||||||
print(f"✅ Config loaded: {len(kids)} kid(s), {total_chores} total chores")
|
print(f"✅ Config loaded: {len(kids)} kid(s), {total_chores} total chores")
|
||||||
for k in kids:
|
for k in kids:
|
||||||
chores = get_chores(k)
|
chores = get_chores(k)
|
||||||
print(f" {k['avatar']} {k['name']}: {len(chores)} chores — {', '.join(c['name'] for c in chores)}")
|
print(f" {k['avatar']} {k['name']}: {len(chores)} chores"
|
||||||
|
f" — {', '.join(c['name'] for c in chores)}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
files = {
|
files = {
|
||||||
@@ -886,9 +940,9 @@ def main():
|
|||||||
|
|
||||||
print()
|
print()
|
||||||
print("✅ Done!")
|
print("✅ Done!")
|
||||||
print(" 1. Flash chore-tracker-esphome.yaml → ESPHome dashboard")
|
print(" 1. Flash chore-tracker-esphome.yaml -> ESPHome dashboard")
|
||||||
print(" 2. Merge chore-tracker-ha.yaml → HA config + restart HA")
|
print(" 2. Merge chore-tracker-ha.yaml -> HA config + restart HA")
|
||||||
print(" 3. Paste chore-tracker-dashboard.yaml → New HA dashboard")
|
print(" 3. Paste chore-tracker-dashboard.yaml -> New HA dashboard")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -35,18 +35,45 @@ captive_portal:
|
|||||||
web_server:
|
web_server:
|
||||||
port: 80
|
port: 80
|
||||||
|
|
||||||
|
# GPIO Outputs for fan control
|
||||||
|
# IMPORTANT: inverted: true because NPN transistor driver inverts the logic
|
||||||
|
# GPIO HIGH -> NPN ON -> Gate LOW -> Fan OFF
|
||||||
|
# GPIO LOW -> NPN OFF -> Gate HIGH (5V via pull-up) -> Fan ON
|
||||||
output:
|
output:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin: GPIO3
|
pin:
|
||||||
|
number: GPIO3 # D1 on XIAO
|
||||||
|
inverted: true
|
||||||
id: fan_80mm_output
|
id: fan_80mm_output
|
||||||
|
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin: GPIO4
|
pin:
|
||||||
|
number: GPIO4 # D2 on XIAO
|
||||||
|
inverted: true
|
||||||
id: fan_120mm_output
|
id: fan_120mm_output
|
||||||
|
|
||||||
|
# Fan entities for Home Assistant
|
||||||
fan:
|
fan:
|
||||||
- platform: binary
|
- platform: binary
|
||||||
name: "80mm Exhaust Fan"
|
name: "80mm Exhaust Fan"
|
||||||
|
id: fan_80mm
|
||||||
output: fan_80mm_output
|
output: fan_80mm_output
|
||||||
|
|
||||||
- platform: binary
|
- platform: binary
|
||||||
name: "120mm Exhaust Fan"
|
name: "120mm Exhaust Fan"
|
||||||
|
id: fan_120mm
|
||||||
output: fan_120mm_output
|
output: fan_120mm_output
|
||||||
|
|
||||||
|
# Optional: Add a switch to control both fans together
|
||||||
|
switch:
|
||||||
|
- platform: template
|
||||||
|
name: "All Fans"
|
||||||
|
id: all_fans
|
||||||
|
turn_on_action:
|
||||||
|
- fan.turn_on: fan_80mm
|
||||||
|
- fan.turn_on: fan_120mm
|
||||||
|
turn_off_action:
|
||||||
|
- fan.turn_off: fan_80mm
|
||||||
|
- fan.turn_off: fan_120mm
|
||||||
|
lambda: |-
|
||||||
|
return id(fan_80mm).state && id(fan_120mm).state;
|
||||||
335
esphome/litter-box-cam.yaml
Normal file
335
esphome/litter-box-cam.yaml
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# LITTER BOX MONITOR - DFRobot ESP32-S3 AI Camera
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
substitutions:
|
||||||
|
device_name: litter-box-cam
|
||||||
|
friendly_name: "Litter Box Camera"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Core Device Configuration
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
esphome:
|
||||||
|
name: ${device_name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
platformio_options:
|
||||||
|
build_flags: "-DBOARD_HAS_PSRAM"
|
||||||
|
board_build.arduino.memory_type: qio_opi
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32-s3-devkitc-1
|
||||||
|
framework:
|
||||||
|
type: arduino
|
||||||
|
version: latest
|
||||||
|
flash_size: 16MB
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# PSRAM Configuration (Critical for camera)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
psram:
|
||||||
|
mode: octal
|
||||||
|
speed: 80MHz
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Logging & API
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logger:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret api_encryption_key
|
||||||
|
|
||||||
|
services:
|
||||||
|
- service: capture_and_analyze
|
||||||
|
then:
|
||||||
|
- script.execute: capture_image_script
|
||||||
|
|
||||||
|
- service: update_status
|
||||||
|
variables:
|
||||||
|
needs_scooping: bool
|
||||||
|
litter_level_low: bool
|
||||||
|
cat_present: bool
|
||||||
|
cleanliness_score: int
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(needs_scooping_sensor).publish_state(needs_scooping);
|
||||||
|
id(litter_level_low_sensor).publish_state(litter_level_low);
|
||||||
|
id(cat_present_sensor).publish_state(cat_present);
|
||||||
|
id(cleanliness_score_sensor).publish_state(cleanliness_score);
|
||||||
|
id(last_analysis_time).publish_state(id(homeassistant_time).now().timestamp);
|
||||||
|
if (cat_present) {
|
||||||
|
id(daily_cat_visits) += 1;
|
||||||
|
id(cat_visits_sensor).publish_state(id(daily_cat_visits));
|
||||||
|
}
|
||||||
|
|
||||||
|
ota:
|
||||||
|
- platform: esphome
|
||||||
|
password: !secret ota_password
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# WiFi Configuration
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
wifi:
|
||||||
|
ssid: !secret wifi_iot_ssid
|
||||||
|
password: !secret wifi_password
|
||||||
|
|
||||||
|
ap:
|
||||||
|
ssid: "${device_name}-AP"
|
||||||
|
password: !secret wifi_password
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# MQTT Configuration
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
mqtt:
|
||||||
|
id: mqtt_client
|
||||||
|
broker: !secret mqtt_broker
|
||||||
|
username: !secret mqtt_username
|
||||||
|
password: !secret mqtt_password
|
||||||
|
topic_prefix: litter_box
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Time Synchronization
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
time:
|
||||||
|
- platform: homeassistant
|
||||||
|
id: homeassistant_time
|
||||||
|
on_time:
|
||||||
|
- seconds: 0
|
||||||
|
minutes: 0
|
||||||
|
hours: 0
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(daily_cat_visits) = 0;
|
||||||
|
id(cat_visits_sensor).publish_state(0);
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Global Variables
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
globals:
|
||||||
|
- id: daily_cat_visits
|
||||||
|
type: int
|
||||||
|
restore_value: yes
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
- id: motion_cooldown_active
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
|
||||||
|
- id: publish_next_image
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Camera Configuration (OV3660 - DFR1154 Specific)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
esp32_camera:
|
||||||
|
id: litter_cam
|
||||||
|
name: "Litter Box Camera"
|
||||||
|
|
||||||
|
# Hardware Interface - DFR1154 Pinout
|
||||||
|
external_clock:
|
||||||
|
pin: GPIO5
|
||||||
|
frequency: 20MHz
|
||||||
|
i2c_pins:
|
||||||
|
sda: GPIO8
|
||||||
|
scl: GPIO9
|
||||||
|
data_pins: [GPIO16, GPIO18, GPIO21, GPIO17, GPIO14, GPIO7, GPIO6, GPIO4]
|
||||||
|
vsync_pin: GPIO1
|
||||||
|
href_pin: GPIO2
|
||||||
|
pixel_clock_pin: GPIO15
|
||||||
|
|
||||||
|
# Image Parameters
|
||||||
|
max_framerate: 10 fps
|
||||||
|
idle_framerate: 0.1 fps
|
||||||
|
resolution: 800x600
|
||||||
|
jpeg_quality: 10
|
||||||
|
vertical_flip: true
|
||||||
|
horizontal_mirror: false
|
||||||
|
aec_mode: AUTO
|
||||||
|
agc_mode: AUTO
|
||||||
|
wb_mode: AUTO
|
||||||
|
brightness: 0
|
||||||
|
contrast: 1
|
||||||
|
saturation: 0
|
||||||
|
on_image:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (!id(publish_next_image)) return;
|
||||||
|
id(publish_next_image) = false;
|
||||||
|
|
||||||
|
const uint8_t *data = x->get_data();
|
||||||
|
size_t len = x->get_data_length();
|
||||||
|
size_t out_len = 4 * ((len + 2) / 3);
|
||||||
|
char *buf = (char *)malloc(out_len + 1);
|
||||||
|
if (!buf) return;
|
||||||
|
|
||||||
|
static const char b64[] =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
size_t j = 0;
|
||||||
|
for (size_t i = 0; i < len; i += 3) {
|
||||||
|
uint32_t b = ((uint32_t)data[i] << 16);
|
||||||
|
if (i + 1 < len) b |= ((uint32_t)data[i + 1] << 8);
|
||||||
|
if (i + 2 < len) b |= data[i + 2];
|
||||||
|
buf[j++] = b64[(b >> 18) & 0x3F];
|
||||||
|
buf[j++] = b64[(b >> 12) & 0x3F];
|
||||||
|
buf[j++] = (i + 1 < len) ? b64[(b >> 6) & 0x3F] : '=';
|
||||||
|
buf[j++] = (i + 2 < len) ? b64[b & 0x3F] : '=';
|
||||||
|
}
|
||||||
|
buf[j] = '\0';
|
||||||
|
|
||||||
|
id(mqtt_client).publish("litter_box/camera/image", buf);
|
||||||
|
ESP_LOGI("capture", "Published image (%d bytes raw, %d base64)", len, j);
|
||||||
|
id(status_message).publish_state("Image sent for analysis");
|
||||||
|
free(buf);
|
||||||
|
|
||||||
|
# Web server for manual viewing
|
||||||
|
esp32_camera_web_server:
|
||||||
|
- port: 8080
|
||||||
|
mode: stream
|
||||||
|
- port: 8081
|
||||||
|
mode: snapshot
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# GPIO Outputs
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
output:
|
||||||
|
- id: led_status
|
||||||
|
platform: gpio
|
||||||
|
pin: GPIO3
|
||||||
|
|
||||||
|
- id: ir_led
|
||||||
|
platform: gpio
|
||||||
|
pin: GPIO47
|
||||||
|
|
||||||
|
- id: speaker_gain
|
||||||
|
platform: gpio
|
||||||
|
pin: GPIO41
|
||||||
|
|
||||||
|
- id: speaker_mode
|
||||||
|
platform: gpio
|
||||||
|
pin: GPIO40
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Lights
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
light:
|
||||||
|
- platform: binary
|
||||||
|
name: "Status LED"
|
||||||
|
id: status_led
|
||||||
|
output: led_status
|
||||||
|
|
||||||
|
- platform: binary
|
||||||
|
name: "IR Night Vision"
|
||||||
|
id: ir_night_vision
|
||||||
|
output: ir_led
|
||||||
|
restore_mode: RESTORE_DEFAULT_OFF
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Binary Sensors
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
binary_sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "Motion Detected"
|
||||||
|
id: motion_detected
|
||||||
|
device_class: motion
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Needs Scooping"
|
||||||
|
id: needs_scooping_sensor
|
||||||
|
device_class: problem
|
||||||
|
icon: "mdi:shovel"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Litter Level Low"
|
||||||
|
id: litter_level_low_sensor
|
||||||
|
device_class: problem
|
||||||
|
icon: "mdi:cup-outline"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Cat Present"
|
||||||
|
id: cat_present_sensor
|
||||||
|
device_class: occupancy
|
||||||
|
icon: "mdi:cat"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Sensors
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
sensor:
|
||||||
|
- platform: wifi_signal
|
||||||
|
name: "WiFi Signal"
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Cleanliness Score"
|
||||||
|
id: cleanliness_score_sensor
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
icon: "mdi:star"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Daily Cat Visits"
|
||||||
|
id: cat_visits_sensor
|
||||||
|
icon: "mdi:paw"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Last Analysis"
|
||||||
|
id: last_analysis_time
|
||||||
|
device_class: timestamp
|
||||||
|
icon: "mdi:clock-outline"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Text Sensors
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
text_sensor:
|
||||||
|
- platform: version
|
||||||
|
name: "ESPHome Version"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Status Message"
|
||||||
|
id: status_message
|
||||||
|
icon: "mdi:message-text"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Switches
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
switch:
|
||||||
|
- platform: restart
|
||||||
|
name: "Restart Camera"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Auto Capture Enabled"
|
||||||
|
id: auto_capture_enabled
|
||||||
|
optimistic: true
|
||||||
|
restore_mode: RESTORE_DEFAULT_ON
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Buttons
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Capture Now"
|
||||||
|
icon: "mdi:camera"
|
||||||
|
on_press:
|
||||||
|
- script.execute: capture_image_script
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Scripts
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
script:
|
||||||
|
- id: capture_image_script
|
||||||
|
mode: single
|
||||||
|
then:
|
||||||
|
- lambda: 'ESP_LOGI("capture", "Capturing image...");'
|
||||||
|
- light.turn_on: status_led
|
||||||
|
- delay: 200ms
|
||||||
|
- lambda: |-
|
||||||
|
id(publish_next_image) = true;
|
||||||
|
id(litter_cam).request_image(esphome::esp32_camera::SINGLE_SHOT);
|
||||||
|
- light.turn_off: status_led
|
||||||
|
- lambda: 'id(motion_cooldown_active) = true;'
|
||||||
|
- delay: 30s
|
||||||
|
- lambda: 'id(motion_cooldown_active) = false;'
|
||||||
293
esphome/master_bedroom_remote.yaml
Normal file
293
esphome/master_bedroom_remote.yaml
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
substitutions:
|
||||||
|
name: mbr-ha-remote
|
||||||
|
friendly_name: "MBR HA Remote"
|
||||||
|
|
||||||
|
# Home Assistant entity IDs - UPDATE THESE TO MATCH YOUR SETUP
|
||||||
|
light_1_entity: switch.pollys_light
|
||||||
|
light_2_entity: switch.joshuas_light
|
||||||
|
fan_entity: switch.parents_ceiling_fan
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
on_boot:
|
||||||
|
priority: -100
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(last_interaction_ms) = millis();
|
||||||
|
id(backlight_is_on) = true;
|
||||||
|
- component.update: my_display
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32dev
|
||||||
|
framework:
|
||||||
|
type: arduino
|
||||||
|
|
||||||
|
logger:
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
# SPI for display and touchscreen (CYD uses two separate SPI buses)
|
||||||
|
spi:
|
||||||
|
- id: tft_spi
|
||||||
|
clk_pin: GPIO14
|
||||||
|
mosi_pin: GPIO13
|
||||||
|
miso_pin: GPIO12
|
||||||
|
- id: touch_spi
|
||||||
|
clk_pin: GPIO25
|
||||||
|
mosi_pin: GPIO32
|
||||||
|
miso_pin: GPIO39
|
||||||
|
|
||||||
|
# ILI9341 Display (2.8" 320x240)
|
||||||
|
display:
|
||||||
|
- platform: ili9xxx
|
||||||
|
model: ili9341
|
||||||
|
spi_id: tft_spi
|
||||||
|
cs_pin: GPIO15
|
||||||
|
dc_pin: GPIO2
|
||||||
|
dimensions:
|
||||||
|
width: 320
|
||||||
|
height: 240
|
||||||
|
# Use palette mode to lower display buffer memory usage.
|
||||||
|
color_palette: 8BIT
|
||||||
|
data_rate: 20MHz
|
||||||
|
update_interval: never
|
||||||
|
rotation: 0
|
||||||
|
invert_colors: false
|
||||||
|
id: my_display
|
||||||
|
lambda: |-
|
||||||
|
it.fill(Color(0x1A1A2E));
|
||||||
|
|
||||||
|
it.print(160, 20, id(title_font), Color(0xFFFFFF), TextAlign::TOP_CENTER, "Room Remote");
|
||||||
|
|
||||||
|
// Button 1: All Toggle (top-left)
|
||||||
|
if (id(all_state)) {
|
||||||
|
it.filled_rectangle(20, 50, 130, 80, Color(0x4CAF50));
|
||||||
|
} else {
|
||||||
|
it.filled_rectangle(20, 50, 130, 80, Color(0x424242));
|
||||||
|
}
|
||||||
|
it.print(85, 90, id(button_font), Color(0xFFFFFF), TextAlign::CENTER, "Toggle All");
|
||||||
|
|
||||||
|
// Button 2: Light 1 (top-right)
|
||||||
|
if (id(light1_state)) {
|
||||||
|
it.filled_rectangle(170, 50, 130, 80, Color(0x4CAF50));
|
||||||
|
} else {
|
||||||
|
it.filled_rectangle(170, 50, 130, 80, Color(0x424242));
|
||||||
|
}
|
||||||
|
it.print(235, 90, id(button_font), Color(0xFFFFFF), TextAlign::CENTER, "Polly's Light");
|
||||||
|
|
||||||
|
// Button 3: Light 2 (bottom-left)
|
||||||
|
if (id(light2_state)) {
|
||||||
|
it.filled_rectangle(20, 150, 130, 80, Color(0x4CAF50));
|
||||||
|
} else {
|
||||||
|
it.filled_rectangle(20, 150, 130, 80, Color(0x424242));
|
||||||
|
}
|
||||||
|
it.print(85, 190, id(button_font), Color(0xFFFFFF), TextAlign::CENTER, "Joshua's Light");
|
||||||
|
|
||||||
|
// Button 4: Fan (bottom-right)
|
||||||
|
if (id(fan_state)) {
|
||||||
|
it.filled_rectangle(170, 150, 130, 80, Color(0x4CAF50));
|
||||||
|
} else {
|
||||||
|
it.filled_rectangle(170, 150, 130, 80, Color(0x424242));
|
||||||
|
}
|
||||||
|
it.print(235, 190, id(button_font), Color(0xFFFFFF), TextAlign::CENTER, "Ceiling Fan");
|
||||||
|
|
||||||
|
# XPT2046 Touchscreen
|
||||||
|
touchscreen:
|
||||||
|
- platform: xpt2046
|
||||||
|
spi_id: touch_spi
|
||||||
|
cs_pin: GPIO33
|
||||||
|
interrupt_pin: GPIO36
|
||||||
|
calibration:
|
||||||
|
x_min: 280
|
||||||
|
x_max: 3860
|
||||||
|
y_min: 340
|
||||||
|
y_max: 3860
|
||||||
|
on_touch:
|
||||||
|
- lambda: |-
|
||||||
|
uint32_t now = millis();
|
||||||
|
id(last_interaction_ms) = now;
|
||||||
|
|
||||||
|
// Wake backlight on first touch only, don't also trigger a button
|
||||||
|
if (!id(backlight_is_on)) {
|
||||||
|
auto call = id(backlight).turn_on();
|
||||||
|
call.perform();
|
||||||
|
id(backlight_is_on) = true;
|
||||||
|
id(last_button_press_ms) = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce: ignore repeated touch events within 600ms
|
||||||
|
if ((uint32_t)(now - id(last_button_press_ms)) < 600) return;
|
||||||
|
id(last_button_press_ms) = now;
|
||||||
|
|
||||||
|
ESP_LOGD("touch", "Touch at x=%d, y=%d", touch.x, touch.y);
|
||||||
|
|
||||||
|
// Button 1: Toggle All (top-left)
|
||||||
|
if (touch.x >= 20 && touch.x <= 150 && touch.y >= 50 && touch.y <= 130) {
|
||||||
|
ESP_LOGD("touch", "Toggle All pressed");
|
||||||
|
id(light1_state) = !id(light1_state);
|
||||||
|
id(light2_state) = !id(light2_state);
|
||||||
|
id(fan_state) = !id(fan_state);
|
||||||
|
id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);
|
||||||
|
id(svc_toggle_all).execute();
|
||||||
|
|
||||||
|
// Button 2: Polly's Light (top-right)
|
||||||
|
} else if (touch.x >= 170 && touch.x <= 300 && touch.y >= 50 && touch.y <= 130) {
|
||||||
|
ESP_LOGD("touch", "Polly's Light pressed");
|
||||||
|
id(light1_state) = !id(light1_state);
|
||||||
|
id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);
|
||||||
|
id(svc_toggle_polly).execute();
|
||||||
|
|
||||||
|
// Button 3: Joshua's Light (bottom-left)
|
||||||
|
} else if (touch.x >= 20 && touch.x <= 150 && touch.y >= 150 && touch.y <= 230) {
|
||||||
|
ESP_LOGD("touch", "Joshua's Light pressed");
|
||||||
|
id(light2_state) = !id(light2_state);
|
||||||
|
id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);
|
||||||
|
id(svc_toggle_joshua).execute();
|
||||||
|
|
||||||
|
// Button 4: Ceiling Fan (bottom-right)
|
||||||
|
} else if (touch.x >= 170 && touch.x <= 300 && touch.y >= 150 && touch.y <= 230) {
|
||||||
|
ESP_LOGD("touch", "Ceiling Fan pressed");
|
||||||
|
id(fan_state) = !id(fan_state);
|
||||||
|
id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);
|
||||||
|
id(svc_toggle_fan).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
id(my_display).update();
|
||||||
|
|
||||||
|
# Backlight control
|
||||||
|
output:
|
||||||
|
- platform: ledc
|
||||||
|
pin: GPIO21
|
||||||
|
id: backlight_output
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: monochromatic
|
||||||
|
output: backlight_output
|
||||||
|
name: "Display Backlight"
|
||||||
|
id: backlight
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
|
||||||
|
# Fonts
|
||||||
|
font:
|
||||||
|
- file: "gfonts://Nunito"
|
||||||
|
id: title_font
|
||||||
|
size: 24
|
||||||
|
- file: "gfonts://Nunito"
|
||||||
|
id: button_font
|
||||||
|
size: 20
|
||||||
|
|
||||||
|
# Scripts to call HA services (lambdas can't call homeassistant.service directly)
|
||||||
|
script:
|
||||||
|
- id: svc_toggle_all
|
||||||
|
then:
|
||||||
|
- homeassistant.service:
|
||||||
|
service: switch.toggle
|
||||||
|
data:
|
||||||
|
entity_id: ${light_1_entity}
|
||||||
|
- homeassistant.service:
|
||||||
|
service: switch.toggle
|
||||||
|
data:
|
||||||
|
entity_id: ${light_2_entity}
|
||||||
|
- homeassistant.service:
|
||||||
|
service: switch.toggle
|
||||||
|
data:
|
||||||
|
entity_id: ${fan_entity}
|
||||||
|
- id: svc_toggle_polly
|
||||||
|
then:
|
||||||
|
- homeassistant.service:
|
||||||
|
service: switch.toggle
|
||||||
|
data:
|
||||||
|
entity_id: ${light_1_entity}
|
||||||
|
- id: svc_toggle_joshua
|
||||||
|
then:
|
||||||
|
- homeassistant.service:
|
||||||
|
service: switch.toggle
|
||||||
|
data:
|
||||||
|
entity_id: ${light_2_entity}
|
||||||
|
- id: svc_toggle_fan
|
||||||
|
then:
|
||||||
|
- homeassistant.service:
|
||||||
|
service: switch.toggle
|
||||||
|
data:
|
||||||
|
entity_id: ${fan_entity}
|
||||||
|
|
||||||
|
# Global state tracking
|
||||||
|
globals:
|
||||||
|
- id: all_state
|
||||||
|
type: bool
|
||||||
|
initial_value: "false"
|
||||||
|
- id: light1_state
|
||||||
|
type: bool
|
||||||
|
initial_value: "false"
|
||||||
|
- id: light2_state
|
||||||
|
type: bool
|
||||||
|
initial_value: "false"
|
||||||
|
- id: fan_state
|
||||||
|
type: bool
|
||||||
|
initial_value: "false"
|
||||||
|
- id: last_interaction_ms
|
||||||
|
type: uint32_t
|
||||||
|
initial_value: "0"
|
||||||
|
- id: last_button_press_ms
|
||||||
|
type: uint32_t
|
||||||
|
initial_value: "0"
|
||||||
|
- id: backlight_is_on
|
||||||
|
type: bool
|
||||||
|
initial_value: "true"
|
||||||
|
|
||||||
|
# Backlight timeout
|
||||||
|
interval:
|
||||||
|
- interval: 100ms
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(backlight_is_on) && ((uint32_t)(millis() - id(last_interaction_ms)) > 30000);'
|
||||||
|
then:
|
||||||
|
- light.turn_off: backlight
|
||||||
|
- lambda: 'id(backlight_is_on) = false;'
|
||||||
|
|
||||||
|
# Import states from Home Assistant
|
||||||
|
text_sensor:
|
||||||
|
- platform: homeassistant
|
||||||
|
entity_id: ${light_1_entity}
|
||||||
|
id: ha_light1_state
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- lambda: 'id(light1_state) = (x == "on");'
|
||||||
|
- lambda: 'id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);'
|
||||||
|
- component.update: my_display
|
||||||
|
|
||||||
|
- platform: homeassistant
|
||||||
|
entity_id: ${light_2_entity}
|
||||||
|
id: ha_light2_state
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- lambda: 'id(light2_state) = (x == "on");'
|
||||||
|
- lambda: 'id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);'
|
||||||
|
- component.update: my_display
|
||||||
|
|
||||||
|
- platform: homeassistant
|
||||||
|
entity_id: ${fan_entity}
|
||||||
|
id: ha_fan_state
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- lambda: 'id(fan_state) = (x == "on");'
|
||||||
|
- lambda: 'id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);'
|
||||||
|
- component.update: my_display
|
||||||
3
esphome/spi_helper.h
Normal file
3
esphome/spi_helper.h
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "driver/spi_master.h"
|
||||||
|
#include "driver/gpio.h"
|
||||||
@@ -16,7 +16,6 @@ esphome:
|
|||||||
switch.is_off: mute
|
switch.is_off: mute
|
||||||
then:
|
then:
|
||||||
- micro_wake_word.start:
|
- micro_wake_word.start:
|
||||||
|
|
||||||
esp32:
|
esp32:
|
||||||
board: esp32-s3-devkitc-1
|
board: esp32-s3-devkitc-1
|
||||||
framework:
|
framework:
|
||||||
@@ -24,7 +23,7 @@ esp32:
|
|||||||
|
|
||||||
# Enable logging
|
# Enable logging
|
||||||
logger:
|
logger:
|
||||||
level: WARN
|
level: DEBUG
|
||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
@@ -59,7 +58,7 @@ light:
|
|||||||
rgb_order: GRB
|
rgb_order: GRB
|
||||||
chipset: ws2812
|
chipset: ws2812
|
||||||
pin: GPIO16
|
pin: GPIO16
|
||||||
num_leds: 4
|
num_leds: 8
|
||||||
name: "LED bar"
|
name: "LED bar"
|
||||||
effects:
|
effects:
|
||||||
- pulse:
|
- pulse:
|
||||||
@@ -72,6 +71,23 @@ light:
|
|||||||
move_interval: 100ms
|
move_interval: 100ms
|
||||||
scan_width: 1
|
scan_width: 1
|
||||||
|
|
||||||
|
number:
|
||||||
|
- platform: template
|
||||||
|
id: va_volume_percent
|
||||||
|
name: "VA Speaker Volume"
|
||||||
|
min_value: 10
|
||||||
|
max_value: 100
|
||||||
|
step: 5
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
mode: slider
|
||||||
|
optimistic: true
|
||||||
|
restore_value: true
|
||||||
|
initial_value: 80
|
||||||
|
set_action:
|
||||||
|
- speaker.volume_set:
|
||||||
|
id: va_speaker
|
||||||
|
volume: !lambda "return x / 100.0f;"
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: template
|
- platform: template
|
||||||
id: mute
|
id: mute
|
||||||
@@ -150,6 +166,7 @@ micro_wake_word:
|
|||||||
- model: hey_jarvis
|
- model: hey_jarvis
|
||||||
on_wake_word_detected:
|
on_wake_word_detected:
|
||||||
- voice_assistant.start:
|
- voice_assistant.start:
|
||||||
|
wake_word: "Hey Jarvis"
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: led_ww
|
id: led_ww
|
||||||
red: 100%
|
red: 100%
|
||||||
@@ -162,9 +179,10 @@ voice_assistant:
|
|||||||
id: va
|
id: va
|
||||||
microphone: va_mic
|
microphone: va_mic
|
||||||
speaker: va_speaker
|
speaker: va_speaker
|
||||||
|
use_wake_word: false
|
||||||
noise_suppression_level: 2.0
|
noise_suppression_level: 2.0
|
||||||
auto_gain: 31dBFS
|
auto_gain: 31dBFS
|
||||||
volume_multiplier: 4.0
|
volume_multiplier: 8.0
|
||||||
on_client_connected:
|
on_client_connected:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
@@ -178,11 +196,23 @@ voice_assistant:
|
|||||||
then:
|
then:
|
||||||
- light.turn_off: led_ww
|
- light.turn_off: led_ww
|
||||||
on_error:
|
on_error:
|
||||||
- micro_wake_word.start:
|
- light.turn_on:
|
||||||
|
id: led_ww
|
||||||
|
red: 100%
|
||||||
|
green: 0%
|
||||||
|
blue: 0%
|
||||||
|
brightness: 30%
|
||||||
|
effect: fast pulse
|
||||||
|
- delay: 1s
|
||||||
|
- light.turn_off: led_ww
|
||||||
on_end:
|
on_end:
|
||||||
then:
|
then:
|
||||||
- light.turn_off: led_ww
|
- light.turn_off: led_ww
|
||||||
- wait_until:
|
- wait_until:
|
||||||
not:
|
not:
|
||||||
voice_assistant.is_running:
|
voice_assistant.is_running:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
switch.is_off: mute
|
||||||
|
then:
|
||||||
- micro_wake_word.start:
|
- micro_wake_word.start:
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
# Kids: Jordyn, Declan, Chloe
|
# Kids: Jordyn, Declan, Chloe
|
||||||
#
|
#
|
||||||
# BIDIRECTIONAL SYNC:
|
# BIDIRECTIONAL SYNC:
|
||||||
# Screen → HA: Each switch calls homeassistant.service on toggle
|
# Screen to HA: Each switch calls homeassistant.service on toggle
|
||||||
# HA → Screen: ESPHome native API handles this automatically
|
# HA to Screen: ESPHome native API handles this automatically
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
input_button:
|
input_button:
|
||||||
@@ -27,163 +27,127 @@ template:
|
|||||||
unique_id: jordyn_chores_done_today
|
unique_id: jordyn_chores_done_today
|
||||||
icon: mdi:check-circle
|
icon: mdi:check-circle
|
||||||
state: >
|
state: >
|
||||||
{% set chores = [states('switch.chore_tracker_jordyn_make_bed'), states('switch.chore_tracker_jordyn_brush_teeth'), states('switch.chore_tracker_jordyn_tidy_room'), states('switch.chore_tracker_jordyn_homework'), states('switch.chore_tracker_jordyn_feed_dog')] %}
|
{% set chores = [states('switch.chore_tracker_jordyn_put_on_underwear'), states('switch.chore_tracker_jordyn_brush_teeth'), states('switch.chore_tracker_jordyn_fill_water_bowls'), states('switch.chore_tracker_jordyn_restock_cat_food_cans')] %}
|
||||||
{{ chores | select('equalto', 'on') | list | count }}
|
{{ chores | select('equalto', 'on') | list | count }}
|
||||||
|
|
||||||
- name: "Jordyn All Chores Done"
|
- name: "Jordyn All Chores Done"
|
||||||
unique_id: jordyn_all_chores_done
|
unique_id: jordyn_all_chores_done
|
||||||
icon: mdi:check-all
|
icon: mdi:check-all
|
||||||
state: >
|
state: >
|
||||||
{{ states('sensor.jordyn_chores_done_today') | int == 5 }}
|
{{ states('sensor.jordyn_chores_done_today') | int == 4 }}
|
||||||
|
|
||||||
- name: "Declan Chores Done Today"
|
- name: "Declan Chores Done Today"
|
||||||
unique_id: declan_chores_done_today
|
unique_id: declan_chores_done_today
|
||||||
icon: mdi:check-circle
|
icon: mdi:check-circle
|
||||||
state: >
|
state: >
|
||||||
{% set chores = [states('switch.chore_tracker_declan_make_bed'), states('switch.chore_tracker_declan_brush_teeth'), states('switch.chore_tracker_declan_set_table'), states('switch.chore_tracker_declan_take_out_trash')] %}
|
{% set chores = [states('switch.chore_tracker_declan_take_morning_pill'), states('switch.chore_tracker_declan_scoop_dog_poop')] %}
|
||||||
{{ chores | select('equalto', 'on') | list | count }}
|
{{ chores | select('equalto', 'on') | list | count }}
|
||||||
|
|
||||||
- name: "Declan All Chores Done"
|
- name: "Declan All Chores Done"
|
||||||
unique_id: declan_all_chores_done
|
unique_id: declan_all_chores_done
|
||||||
icon: mdi:check-all
|
icon: mdi:check-all
|
||||||
state: >
|
state: >
|
||||||
{{ states('sensor.declan_chores_done_today') | int == 4 }}
|
{{ states('sensor.declan_chores_done_today') | int == 2 }}
|
||||||
|
|
||||||
- name: "Chloe Chores Done Today"
|
- name: "Chloe Chores Done Today"
|
||||||
unique_id: chloe_chores_done_today
|
unique_id: chloe_chores_done_today
|
||||||
icon: mdi:check-circle
|
icon: mdi:check-circle
|
||||||
state: >
|
state: >
|
||||||
{% set chores = [states('switch.chore_tracker_chloe_make_bed'), states('switch.chore_tracker_chloe_brush_teeth'), states('switch.chore_tracker_chloe_water_plants'), states('switch.chore_tracker_chloe_homework'), states('switch.chore_tracker_chloe_tidy_room'), states('switch.chore_tracker_chloe_practice_piano')] %}
|
{% set chores = [states('switch.chore_tracker_chloe_fill_kitty_feeders'), states('switch.chore_tracker_chloe_scoop_kitty_litter'), states('switch.chore_tracker_chloe_replace_kitty_litter_bags')] %}
|
||||||
{{ chores | select('equalto', 'on') | list | count }}
|
{{ chores | select('equalto', 'on') | list | count }}
|
||||||
|
|
||||||
- name: "Chloe All Chores Done"
|
- name: "Chloe All Chores Done"
|
||||||
unique_id: chloe_all_chores_done
|
unique_id: chloe_all_chores_done
|
||||||
icon: mdi:check-all
|
icon: mdi:check-all
|
||||||
state: >
|
state: >
|
||||||
{{ states('sensor.chloe_chores_done_today') | int == 6 }}
|
{{ states('sensor.chloe_chores_done_today') | int == 3 }}
|
||||||
|
|
||||||
|
|
||||||
automation:
|
automation:
|
||||||
- id: jordyn_reset_from_ha
|
- id: jordyn_reset_from_ha
|
||||||
alias: "Chore Tracker — Reset Jordyn's chores from HA"
|
alias: "Chore Tracker - Reset Jordyn's chores from HA"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: input_button.jordyn_reset_chores
|
entity_id: input_button.jordyn_reset_chores
|
||||||
action:
|
action:
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_jordyn_make_bed
|
entity_id: switch.chore_tracker_jordyn_put_on_underwear
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_jordyn_brush_teeth
|
entity_id: switch.chore_tracker_jordyn_brush_teeth
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_jordyn_tidy_room
|
entity_id: switch.chore_tracker_jordyn_fill_water_bowls
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_jordyn_homework
|
entity_id: switch.chore_tracker_jordyn_restock_cat_food_cans
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_jordyn_feed_dog
|
|
||||||
|
|
||||||
- id: declan_reset_from_ha
|
- id: declan_reset_from_ha
|
||||||
alias: "Chore Tracker — Reset Declan's chores from HA"
|
alias: "Chore Tracker - Reset Declan's chores from HA"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: input_button.declan_reset_chores
|
entity_id: input_button.declan_reset_chores
|
||||||
action:
|
action:
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_declan_make_bed
|
entity_id: switch.chore_tracker_declan_take_morning_pill
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_declan_brush_teeth
|
entity_id: switch.chore_tracker_declan_scoop_dog_poop
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_declan_set_table
|
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_declan_take_out_trash
|
|
||||||
|
|
||||||
- id: chloe_reset_from_ha
|
- id: chloe_reset_from_ha
|
||||||
alias: "Chore Tracker — Reset Chloe's chores from HA"
|
alias: "Chore Tracker - Reset Chloe's chores from HA"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: input_button.chloe_reset_chores
|
entity_id: input_button.chloe_reset_chores
|
||||||
action:
|
action:
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_chloe_make_bed
|
entity_id: switch.chore_tracker_chloe_fill_kitty_feeders
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_chloe_brush_teeth
|
entity_id: switch.chore_tracker_chloe_scoop_kitty_litter
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_chloe_water_plants
|
entity_id: switch.chore_tracker_chloe_replace_kitty_litter_bags
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_chloe_homework
|
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_chloe_tidy_room
|
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_chloe_practice_piano
|
|
||||||
|
|
||||||
- id: reset_all_from_ha
|
- id: reset_all_from_ha
|
||||||
alias: "Chore Tracker — Reset ALL chores from HA"
|
alias: "Chore Tracker - Reset ALL chores from HA"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: input_button.reset_all_chores
|
entity_id: input_button.reset_all_chores
|
||||||
action:
|
action:
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_jordyn_make_bed
|
entity_id: switch.chore_tracker_jordyn_put_on_underwear
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_jordyn_brush_teeth
|
entity_id: switch.chore_tracker_jordyn_brush_teeth
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_jordyn_tidy_room
|
entity_id: switch.chore_tracker_jordyn_fill_water_bowls
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_jordyn_homework
|
entity_id: switch.chore_tracker_jordyn_restock_cat_food_cans
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_jordyn_feed_dog
|
entity_id: switch.chore_tracker_declan_take_morning_pill
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_declan_make_bed
|
entity_id: switch.chore_tracker_declan_scoop_dog_poop
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_declan_brush_teeth
|
entity_id: switch.chore_tracker_chloe_fill_kitty_feeders
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_declan_set_table
|
entity_id: switch.chore_tracker_chloe_scoop_kitty_litter
|
||||||
- service: switch.turn_off
|
- service: switch.turn_off
|
||||||
target:
|
target:
|
||||||
entity_id: switch.chore_tracker_declan_take_out_trash
|
entity_id: switch.chore_tracker_chloe_replace_kitty_litter_bags
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_chloe_make_bed
|
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_chloe_brush_teeth
|
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_chloe_water_plants
|
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_chloe_homework
|
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_chloe_tidy_room
|
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.chore_tracker_chloe_practice_piano
|
|
||||||
|
|
||||||
- id: jordyn_all_done_notify
|
- id: jordyn_all_done_notify
|
||||||
alias: "Chore Tracker — Jordyn all done!"
|
alias: "Chore Tracker - Jordyn all done!"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: sensor.jordyn_all_chores_done
|
entity_id: sensor.jordyn_all_chores_done
|
||||||
@@ -191,11 +155,11 @@ automation:
|
|||||||
action:
|
action:
|
||||||
- service: notify.notify
|
- service: notify.notify
|
||||||
data:
|
data:
|
||||||
title: "🎉 Jordyn finished all chores!"
|
title: "Jordyn finished all chores!"
|
||||||
message: "Jordyn completed all 5 chores today!"
|
message: "Jordyn completed all 4 chores today!"
|
||||||
|
|
||||||
- id: declan_all_done_notify
|
- id: declan_all_done_notify
|
||||||
alias: "Chore Tracker — Declan all done!"
|
alias: "Chore Tracker - Declan all done!"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: sensor.declan_all_chores_done
|
entity_id: sensor.declan_all_chores_done
|
||||||
@@ -203,11 +167,11 @@ automation:
|
|||||||
action:
|
action:
|
||||||
- service: notify.notify
|
- service: notify.notify
|
||||||
data:
|
data:
|
||||||
title: "🎉 Declan finished all chores!"
|
title: "Declan finished all chores!"
|
||||||
message: "Declan completed all 4 chores today!"
|
message: "Declan completed all 2 chores today!"
|
||||||
|
|
||||||
- id: chloe_all_done_notify
|
- id: chloe_all_done_notify
|
||||||
alias: "Chore Tracker — Chloe all done!"
|
alias: "Chore Tracker - Chloe all done!"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id: sensor.chloe_all_chores_done
|
entity_id: sensor.chloe_all_chores_done
|
||||||
@@ -215,11 +179,11 @@ automation:
|
|||||||
action:
|
action:
|
||||||
- service: notify.notify
|
- service: notify.notify
|
||||||
data:
|
data:
|
||||||
title: "🎉 Chloe finished all chores!"
|
title: "Chloe finished all chores!"
|
||||||
message: "Chloe completed all 6 chores today!"
|
message: "Chloe completed all 3 chores today!"
|
||||||
|
|
||||||
- id: chore_reminder_evening
|
- id: chore_reminder_evening
|
||||||
alias: "Chore Tracker — Evening reminder"
|
alias: "Chore Tracker - Evening reminder"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: time
|
- platform: time
|
||||||
at: "18:00:00"
|
at: "18:00:00"
|
||||||
@@ -230,29 +194,29 @@ automation:
|
|||||||
then:
|
then:
|
||||||
- service: notify.notify
|
- service: notify.notify
|
||||||
data:
|
data:
|
||||||
title: "📋 Jordyn has unfinished chores"
|
title: "Jordyn has unfinished chores"
|
||||||
message: >
|
message: >
|
||||||
Jordyn has done
|
Jordyn has done
|
||||||
{{ states('sensor.jordyn_chores_done_today') }}/5 chores today.
|
{{ states('sensor.jordyn_chores_done_today') }}/4 chores today.
|
||||||
- if:
|
- if:
|
||||||
condition: template
|
condition: template
|
||||||
value_template: "{{ states('sensor.declan_all_chores_done') != 'True' }}"
|
value_template: "{{ states('sensor.declan_all_chores_done') != 'True' }}"
|
||||||
then:
|
then:
|
||||||
- service: notify.notify
|
- service: notify.notify
|
||||||
data:
|
data:
|
||||||
title: "📋 Declan has unfinished chores"
|
title: "Declan has unfinished chores"
|
||||||
message: >
|
message: >
|
||||||
Declan has done
|
Declan has done
|
||||||
{{ states('sensor.declan_chores_done_today') }}/4 chores today.
|
{{ states('sensor.declan_chores_done_today') }}/2 chores today.
|
||||||
- if:
|
- if:
|
||||||
condition: template
|
condition: template
|
||||||
value_template: "{{ states('sensor.chloe_all_chores_done') != 'True' }}"
|
value_template: "{{ states('sensor.chloe_all_chores_done') != 'True' }}"
|
||||||
then:
|
then:
|
||||||
- service: notify.notify
|
- service: notify.notify
|
||||||
data:
|
data:
|
||||||
title: "📋 Chloe has unfinished chores"
|
title: "Chloe has unfinished chores"
|
||||||
message: >
|
message: >
|
||||||
Chloe has done
|
Chloe has done
|
||||||
{{ states('sensor.chloe_chores_done_today') }}/6 chores today.
|
{{ states('sensor.chloe_chores_done_today') }}/3 chores today.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user