Compare commits
161 Commits
0a0ec84845
...
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 | |||
|
|
7c347fd524 | ||
| 8d6359cdeb | |||
|
|
f60e040bc7 | ||
|
|
0a66aa438c | ||
| 705e74d9de | |||
| 4b31b54914 | |||
| ba4d815201 | |||
| 1c575a7185 | |||
| d853f5f54f | |||
|
|
0963024b91 | ||
|
|
ce6eacbebb | ||
|
|
8e619a8361 | ||
|
|
344ed66c36 | ||
|
|
fc8567b6db | ||
|
|
deec641949 | ||
|
|
b026f2937a | ||
|
|
76e40b2fef | ||
|
|
0ae5f13859 | ||
|
|
7316d9c38b | ||
|
|
e7aa4dad9b | ||
| b9913fa8fb | |||
|
|
5dc58e147b | ||
|
|
83a4d116ec | ||
|
|
00e06c80c6 | ||
|
|
929f41674a | ||
|
|
d6ddd44787 | ||
|
|
49d87e7777 | ||
|
|
0c66d45cc6 | ||
|
|
128388f0ac | ||
|
|
f6f2b71851 | ||
| ed7deb6cb8 | |||
| 5dcb436063 | |||
| 918ca361fc | |||
|
|
88a9caabaf | ||
|
|
2eaad35aaa | ||
|
|
c69d955080 | ||
|
|
e75763ba4d | ||
|
|
53c3add254 | ||
|
|
e1715b4ca5 | ||
|
|
c3bf64281c | ||
|
|
ab4a22e20c | ||
|
|
19c088f81f | ||
|
|
9151032d80 | ||
|
|
049130b7fd | ||
|
|
7939382efc | ||
|
|
4a4cf43413 | ||
|
|
5ef090652a | ||
|
|
4c4102469b | ||
|
|
c83884e764 | ||
|
|
86bc59e7ff | ||
| 262450d48f |
@@ -1 +1 @@
|
||||
2026.2.2
|
||||
2026.2.3
|
||||
35
.gitignore
vendored
35
.gitignore
vendored
@@ -8,17 +8,50 @@
|
||||
!*.md
|
||||
!*.sh
|
||||
!*.js*
|
||||
!*.txt*
|
||||
!*.json*
|
||||
!*.ui.yaml
|
||||
!*.py
|
||||
|
||||
# Whitelist subdirectory yaml files
|
||||
!*/*.yaml
|
||||
!*/*.yml
|
||||
!*/*.md
|
||||
!*/*.sh
|
||||
!*/*.js*
|
||||
!*/*.txt*
|
||||
!*/*.json*
|
||||
!*/*.ui.yaml
|
||||
!*/*.h
|
||||
!*/*.py
|
||||
!*/*/*.py
|
||||
!*/*/*.yaml
|
||||
|
||||
|
||||
# Whitelist specific folders (uncomment as needed)
|
||||
!automations/
|
||||
!packages/**
|
||||
!packages/**
|
||||
!packages/chore-tracker-ha.yaml
|
||||
!scenes/
|
||||
!scripts/
|
||||
!blueprints/
|
||||
!esphome/
|
||||
!esphome/fonts/**
|
||||
!esphome/ha-remote/
|
||||
!esphome/ha-remote/components/
|
||||
!esphome/ha-remote/components/**
|
||||
!esphome/ha-remote/components/max17043/
|
||||
!esphome/components/**
|
||||
!esphome/components/max17043/**
|
||||
!custom_components/**
|
||||
!esphome/chore-tracker/*.py
|
||||
!esphome/chore-tracker/*.yaml
|
||||
!esphome/chore-tracker/
|
||||
!esphome/chore-tracker/**
|
||||
!esphome/chore-tracker-esphome.yaml
|
||||
# !packages/
|
||||
# !themes/
|
||||
!blueprints/
|
||||
# !node-red/
|
||||
|
||||
# =============================================
|
||||
|
||||
206
automations.yaml
206
automations.yaml
@@ -224,7 +224,7 @@
|
||||
conditions:
|
||||
- condition: time
|
||||
after: '21:00:00'
|
||||
before: 07:00:00
|
||||
before: 07:30:00
|
||||
actions:
|
||||
- action: notify.mobile_app_joshuas_iphone_of_pain
|
||||
metadata: {}
|
||||
@@ -1031,11 +1031,11 @@
|
||||
- action: notify.mobile_app_joshuas_iphone_of_pain
|
||||
metadata: {}
|
||||
data:
|
||||
message: The Deuce is full of deuces
|
||||
message: The Deuce is full of deuces (laundry room)
|
||||
- action: notify.mobile_app_pollys_iphone
|
||||
metadata: {}
|
||||
data:
|
||||
message: The Deuce is full of deuces
|
||||
message: The Deuce is full of deuces (laundry room)
|
||||
- action: notify.mailgun_smtp
|
||||
metadata: {}
|
||||
data:
|
||||
@@ -1043,6 +1043,10 @@
|
||||
title: Too many dueces have been dropped!
|
||||
target: joshua@cnjmail.com
|
||||
enabled: false
|
||||
- action: notify.mobile_app_chloes_iphone
|
||||
metadata: {}
|
||||
data:
|
||||
message: The Deuce is full of deuces (laundry room)
|
||||
mode: single
|
||||
- id: '1742842372471'
|
||||
alias: The Poop Box has runneth over Notification
|
||||
@@ -1058,11 +1062,15 @@
|
||||
- action: notify.mobile_app_joshuas_iphone_of_pain
|
||||
metadata: {}
|
||||
data:
|
||||
message: The Poop Box has runneth over
|
||||
message: The Poop Box has runneth over (basement, left side)
|
||||
- action: notify.mobile_app_pollys_iphone
|
||||
metadata: {}
|
||||
data:
|
||||
message: The Poop Box has runneth over
|
||||
message: The Poop Box has runneth over (basement, left side)
|
||||
- action: notify.mobile_app_chloes_iphone
|
||||
metadata: {}
|
||||
data:
|
||||
message: The Poop Box has runneth over (basement, left side)
|
||||
- action: notify.mailgun_smtp
|
||||
metadata: {}
|
||||
data:
|
||||
@@ -1085,11 +1093,15 @@
|
||||
- action: notify.mobile_app_joshuas_iphone_of_pain
|
||||
metadata: {}
|
||||
data:
|
||||
message: Triple Threat can't take no more!
|
||||
message: Triple Threat can't take no more! (Basement, right side)
|
||||
- action: notify.mobile_app_pollys_iphone
|
||||
metadata: {}
|
||||
data:
|
||||
message: Triple Threat can't take no more!
|
||||
message: Triple Threat can't take no more! (Basement, right side)
|
||||
- action: notify.mobile_app_chloes_iphone
|
||||
metadata: {}
|
||||
data:
|
||||
message: Triple Threat can't take no more! (Basement, right side)
|
||||
- action: notify.mailgun_smtp
|
||||
metadata: {}
|
||||
data:
|
||||
@@ -1900,44 +1912,6 @@
|
||||
entity_id: 84d24335edc946153ed4f81ac7906f3c
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1770043717316'
|
||||
alias: turn on jordyn a1 cam light
|
||||
description: ''
|
||||
triggers:
|
||||
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
|
||||
domain: bambu_lab
|
||||
type: event_print_started
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- type: turn_on
|
||||
device_id: d174bbc498cde1d02e455cb133c04675
|
||||
entity_id: 0736793ae0d143db76ea7c64d86f4268
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1770045082546'
|
||||
alias: Turn off Jordyn a1 camera
|
||||
description: ''
|
||||
triggers:
|
||||
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
|
||||
domain: bambu_lab
|
||||
type: event_print_finished
|
||||
trigger: device
|
||||
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
|
||||
domain: bambu_lab
|
||||
type: event_print_failed
|
||||
trigger: device
|
||||
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
|
||||
domain: bambu_lab
|
||||
type: event_print_canceled
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- type: turn_off
|
||||
device_id: d174bbc498cde1d02e455cb133c04675
|
||||
entity_id: 0736793ae0d143db76ea7c64d86f4268
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1770433866161'
|
||||
alias: Turn off Kitchen Sink at Sunrise
|
||||
description: ''
|
||||
@@ -1960,6 +1934,10 @@
|
||||
domain: bambu_lab
|
||||
type: event_print_started
|
||||
trigger: device
|
||||
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
|
||||
domain: bambu_lab
|
||||
type: event_print_error_cleared
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- action: light.turn_on
|
||||
@@ -1969,7 +1947,7 @@
|
||||
data: {}
|
||||
- type: turn_on
|
||||
device_id: d174bbc498cde1d02e455cb133c04675
|
||||
entity_id: c1781c1af12384eeafe080e11876b286
|
||||
entity_id: 0736793ae0d143db76ea7c64d86f4268
|
||||
domain: switch
|
||||
- action: light.turn_on
|
||||
metadata: {}
|
||||
@@ -1987,13 +1965,25 @@
|
||||
domain: bambu_lab
|
||||
type: event_print_started
|
||||
trigger: device
|
||||
- device_id: 299743e9ce66334a0f3bac10eb24cf31
|
||||
domain: bambu_lab
|
||||
type: event_print_error_cleared
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- action: light.turn_on
|
||||
metadata: {}
|
||||
target:
|
||||
entity_id: light.a1_03919d540806387_chamber_light
|
||||
data: {}
|
||||
entity_id:
|
||||
- light.a1_03919d540806387_chamber_light
|
||||
- light.led_controller_polly_pocket_declan_s_led_controller
|
||||
data:
|
||||
color_temp_kelvin: 6500
|
||||
brightness_pct: 100
|
||||
- type: turn_on
|
||||
device_id: c63f4513655bdd5b44b998f45b491136
|
||||
entity_id: fe6d0eee24eb1b397d37b7389137b5b5
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1770938855253'
|
||||
alias: Jordyn-a1-light-off-print-send
|
||||
@@ -2003,6 +1993,10 @@
|
||||
domain: bambu_lab
|
||||
type: event_print_finished
|
||||
trigger: device
|
||||
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
|
||||
domain: bambu_lab
|
||||
type: event_print_canceled
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- action: light.turn_off
|
||||
@@ -2015,7 +2009,7 @@
|
||||
transition: 60
|
||||
- type: turn_off
|
||||
device_id: d174bbc498cde1d02e455cb133c04675
|
||||
entity_id: c1781c1af12384eeafe080e11876b286
|
||||
entity_id: 0736793ae0d143db76ea7c64d86f4268
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1770943298493'
|
||||
@@ -2024,15 +2018,25 @@
|
||||
triggers:
|
||||
- device_id: 299743e9ce66334a0f3bac10eb24cf31
|
||||
domain: bambu_lab
|
||||
type: event_print_started
|
||||
type: event_print_finished
|
||||
trigger: device
|
||||
- device_id: 299743e9ce66334a0f3bac10eb24cf31
|
||||
domain: bambu_lab
|
||||
type: event_print_canceled
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- action: light.turn_off
|
||||
metadata: {}
|
||||
target:
|
||||
entity_id: light.a1_03919d540806387_chamber_light
|
||||
entity_id:
|
||||
- light.a1_03919d540806387_chamber_light
|
||||
- light.led_controller_polly_pocket_declan_s_led_controller
|
||||
data: {}
|
||||
- type: turn_off
|
||||
device_id: c63f4513655bdd5b44b998f45b491136
|
||||
entity_id: fe6d0eee24eb1b397d37b7389137b5b5
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1770943760193'
|
||||
alias: Jordyn-A1-Flash-Red-When-Print-Error
|
||||
@@ -2042,6 +2046,10 @@
|
||||
domain: bambu_lab
|
||||
type: event_print_error
|
||||
trigger: device
|
||||
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
|
||||
domain: bambu_lab
|
||||
type: event_print_failed
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- action: light.turn_on
|
||||
@@ -2055,24 +2063,15 @@
|
||||
- 0
|
||||
effect: Flash
|
||||
brightness_pct: 100
|
||||
mode: single
|
||||
- id: '1770943916864'
|
||||
alias: Jordyn-A1-LED-White-When-Error-Cleared
|
||||
description: ''
|
||||
triggers:
|
||||
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
|
||||
domain: bambu_lab
|
||||
type: event_print_error_cleared
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- action: light.turn_on
|
||||
- type: turn_off
|
||||
device_id: d174bbc498cde1d02e455cb133c04675
|
||||
entity_id: 0736793ae0d143db76ea7c64d86f4268
|
||||
domain: switch
|
||||
- action: light.turn_off
|
||||
metadata: {}
|
||||
target:
|
||||
entity_id: light.jordyn_s_led_controller_jordyn_s_led_controller
|
||||
data:
|
||||
color_temp_kelvin: 6500
|
||||
brightness_pct: 100
|
||||
entity_id: light.jordyns_layer_slayer_chamber_light
|
||||
data: {}
|
||||
mode: single
|
||||
- id: '1770944104889'
|
||||
alias: Pints-Charming-Lights-On-Print-Started
|
||||
@@ -2134,7 +2133,76 @@
|
||||
actions:
|
||||
- action: light.turn_off
|
||||
metadata: {}
|
||||
target:
|
||||
entity_id: light.constipation_orion_chamber_light
|
||||
data: {}
|
||||
target:
|
||||
entity_id: light.p1s_01p09c470102673_chamber_light
|
||||
mode: single
|
||||
- id: '1771090372998'
|
||||
alias: Declans Closet Door Battery Low Notification
|
||||
description: ''
|
||||
triggers:
|
||||
- trigger: numeric_state
|
||||
entity_id:
|
||||
- sensor.declans_closet_door_battery
|
||||
below: 30
|
||||
conditions: []
|
||||
actions:
|
||||
- action: notify.mobile_app_pollys_iphone
|
||||
metadata: {}
|
||||
data:
|
||||
message: Declan's closet sensor battery is low.
|
||||
- action: notify.mobile_app_joshuas_iphone_of_pain
|
||||
metadata: {}
|
||||
data:
|
||||
message: Declan's closet sensor battery is low.
|
||||
mode: single
|
||||
- id: '1771272500745'
|
||||
alias: polly-pocket-a1-mini-lights-on-during-print
|
||||
description: ''
|
||||
triggers:
|
||||
- device_id: 1cf63ab34db5ac50291c1477cddf2b9a
|
||||
domain: bambu_lab
|
||||
type: event_print_started
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- action: light.turn_on
|
||||
metadata: {}
|
||||
target:
|
||||
entity_id:
|
||||
- light.a1mini_0309ca580403653_chamber_light
|
||||
- light.polly_s_led_controller_polly_s_led_controller
|
||||
data:
|
||||
color_temp_kelvin: 6500
|
||||
brightness_pct: 100
|
||||
- type: turn_on
|
||||
device_id: a78c9554b983d1e2459194cc0fee22d3
|
||||
entity_id: d430d19aa2f118038cb01a3157865ed0
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1771272773906'
|
||||
alias: polly-pocket-a1-mini-lights-off-during-print
|
||||
description: ''
|
||||
triggers:
|
||||
- device_id: 1cf63ab34db5ac50291c1477cddf2b9a
|
||||
domain: bambu_lab
|
||||
type: event_print_finished
|
||||
trigger: device
|
||||
- device_id: 1cf63ab34db5ac50291c1477cddf2b9a
|
||||
domain: bambu_lab
|
||||
type: event_print_canceled
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- action: light.turn_off
|
||||
metadata: {}
|
||||
target:
|
||||
entity_id:
|
||||
- light.a1mini_0309ca580403653_chamber_light
|
||||
- light.polly_s_led_controller_polly_s_led_controller
|
||||
data: {}
|
||||
- type: turn_off
|
||||
device_id: a78c9554b983d1e2459194cc0fee22d3
|
||||
entity_id: d430d19aa2f118038cb01a3157865ed0
|
||||
domain: switch
|
||||
mode: single
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
homeassistant:
|
||||
packages: !include_dir_named packages/
|
||||
|
||||
# Loads default set of integrations. Do not remove.
|
||||
default_config:
|
||||
|
||||
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
|
||||
47
esphome/MAX17048_component.h
Normal file
47
esphome/MAX17048_component.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "esphome.h"
|
||||
|
||||
#define MAX17048_ADDRESS 0x36
|
||||
#define MAX17048_VCELL 0x02 // voltage
|
||||
#define MAX17048_SOC 0x04 // percentage
|
||||
#define MAX17048_MODE 0x06
|
||||
#define MAX17048_VERSION 0x08
|
||||
#define MAX17048_CONFIG 0x0c
|
||||
#define MAX17048_COMMAND 0xfe
|
||||
|
||||
class MAX17048Sensor : public PollingComponent, public Sensor {
|
||||
public:
|
||||
Sensor *voltage_sensor = new Sensor();
|
||||
Sensor *percentage_sensor = new Sensor();
|
||||
|
||||
MAX17048Sensor() : PollingComponent(10000) {}
|
||||
|
||||
void setup() override {
|
||||
// Initialize the device here. Usually Wire.begin() will be called in here,
|
||||
// though that call is unnecessary if you have an 'i2c:' entry in your config
|
||||
ESP_LOGD("custom", "Starting up MAX17048 sensor");
|
||||
|
||||
Wire.begin();
|
||||
}
|
||||
|
||||
uint16_t read16(uint8_t reg) {
|
||||
uint16_t temp;
|
||||
Wire.begin();
|
||||
Wire.beginTransmission(MAX17048_ADDRESS);
|
||||
Wire.write(reg);
|
||||
Wire.endTransmission();
|
||||
Wire.requestFrom(MAX17048_ADDRESS, 2);
|
||||
temp = (uint16_t)Wire.read() << 8;
|
||||
temp |= (uint16_t)Wire.read();
|
||||
Wire.endTransmission();
|
||||
return temp;
|
||||
}
|
||||
|
||||
void update() override {
|
||||
float voltage = (float)(read16(MAX17048_VCELL)) * 78.125 / 1000000;
|
||||
voltage_sensor->publish_state(voltage);
|
||||
|
||||
uint16_t percentage_tmp = read16(MAX17048_SOC);
|
||||
float percentage = (float)(percentage_tmp) / 256;
|
||||
percentage_sensor->publish_state(percentage);
|
||||
}
|
||||
};
|
||||
781
esphome/airqualitysensor-1.yaml
Normal file
781
esphome/airqualitysensor-1.yaml
Normal file
@@ -0,0 +1,781 @@
|
||||
substitutions: #substitute your own values in this section
|
||||
internal_temp_sensor: sensor.meat_heater_current_temperature #entity from Home Assistant
|
||||
outside_temp_sensor: sensor.home_realfeel_temperature #entity from Home Assistant
|
||||
weather_entity: weather.home #entity from Home Assistant
|
||||
todays_forecast_high_entity: sensor.home_realfeel_temperature_max_day_0 #entity from Home Assistant
|
||||
todays_forecast_low_entity: sensor.home_realfeel_temperature_min_day_0 #entity from Home Assistant
|
||||
|
||||
esphome:
|
||||
name: airqualitysensor-1
|
||||
friendly_name: AirQualitySensor-1
|
||||
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- light.turn_on:
|
||||
id: led
|
||||
brightness: 0.4
|
||||
effect: "Rainbow Effect"
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
cpu_frequency: 240MHz
|
||||
variant: esp32s3
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: "gnQzjc98wQKUP3qX6+FeU9JchMPtjJwlKQejOB/mQDU="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "d4d36e1c98d4b7e2069295540a20a1aa"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# manual_ip:
|
||||
# static_ip: 192.168.1.61
|
||||
# gateway: 192.168.1.1
|
||||
# subnet: 255.255.255.0
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "Airqualitysensor-1"
|
||||
password: "MpBryi70smN4"
|
||||
|
||||
captive_portal:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: homeassistant_time
|
||||
|
||||
on_time:
|
||||
- seconds: 0
|
||||
minutes: 0
|
||||
hours: 6 # 6 AM
|
||||
then:
|
||||
- logger.log: "Automatic morning turn ON OLED"
|
||||
- switch.turn_on: oled_power
|
||||
|
||||
- seconds: 0
|
||||
minutes: 0
|
||||
hours: 19 # 7 PM
|
||||
then:
|
||||
- logger.log: "Automatic evening turn OFF OLED"
|
||||
- switch.turn_off: oled_power
|
||||
|
||||
script:
|
||||
# SCRIPT 1: Runs when the mode is changed from the select
|
||||
- id: update_led_state
|
||||
then:
|
||||
- lambda: |-
|
||||
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
|
||||
return; // Exit script, leave rainbow alone
|
||||
}
|
||||
|
||||
std::string quality = id(iaq_reading).state;
|
||||
float lux = id(ambient_light).state;
|
||||
std::string mode = id(led_mode).state;
|
||||
|
||||
float brightness = 0.15;
|
||||
float r = 0.0, g = 0.0, b = 0.0;
|
||||
std::string selected_effect = "None";
|
||||
|
||||
// --- 1. Determine desired color ---
|
||||
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
|
||||
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
|
||||
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
|
||||
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
|
||||
else if (quality == "Unhealthy") {
|
||||
r = 1.0; g = 0.0; b = 1.0;
|
||||
selected_effect = "Alert Flash"; // Alert overrides mode
|
||||
}
|
||||
|
||||
// --- 2. Determine desired effect based on mode (if not alerting) ---
|
||||
if (selected_effect != "Alert Flash") {
|
||||
if (mode == "Breathing") {
|
||||
selected_effect = "Breathing";
|
||||
} else if (mode == "Scanner") {
|
||||
selected_effect = "Scanner";
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Apply changes ---
|
||||
auto call = id(led).turn_on();
|
||||
call.set_rgb(r, g, b); // Always set color
|
||||
|
||||
if (selected_effect == "None") {
|
||||
// We want a solid light (Auto/Manual)
|
||||
if (mode == "Auto") {
|
||||
if (isnan(lux)) lux = 0;
|
||||
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
|
||||
if (brightness > 0.7) brightness = 0.7;
|
||||
if (brightness < 0.15) brightness = 0.15;
|
||||
} else { // Manual
|
||||
brightness = id(iaq_led_brightness).state;
|
||||
}
|
||||
call.set_brightness(brightness);
|
||||
}
|
||||
|
||||
// This script *always* sets the effect
|
||||
call.set_effect(selected_effect);
|
||||
call.perform();
|
||||
|
||||
# SCRIPT 2: Runs when IAQ *value* changes (every 10s)
|
||||
- id: update_led_color
|
||||
then:
|
||||
- lambda: |-
|
||||
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
|
||||
return; // Exit script, leave rainbow alone
|
||||
}
|
||||
|
||||
std::string quality = id(iaq_reading).state;
|
||||
std::string mode = id(led_mode).state;
|
||||
float lux = id(ambient_light).state;
|
||||
|
||||
float r = 0.0, g = 0.0, b = 0.0;
|
||||
|
||||
// --- 1. Determine desired color ---
|
||||
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
|
||||
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
|
||||
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
|
||||
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
|
||||
else if (quality == "Unhealthy") {
|
||||
r = 1.0; g = 0.0; b = 1.0;
|
||||
// Force Alert Flash and EXIT
|
||||
id(led).turn_on().set_rgb(r, g, b).set_effect("Alert Flash").perform();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 2. If we are here, AQ is OK. Reset Effect! ---
|
||||
auto call = id(led).turn_on();
|
||||
call.set_rgb(r, g, b);
|
||||
|
||||
// Determine which effect to restore based on the Select Mode
|
||||
std::string restore_effect = "None";
|
||||
if (mode == "Breathing") restore_effect = "Breathing";
|
||||
else if (mode == "Scanner") restore_effect = "Scanner";
|
||||
|
||||
// Explicitly set the effect (This stops the Alert Flash)
|
||||
call.set_effect(restore_effect);
|
||||
|
||||
// --- 3. Apply Brightness ---
|
||||
if (mode == "Auto") {
|
||||
float brightness = 0.15;
|
||||
if (isnan(lux)) lux = 0;
|
||||
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
|
||||
if (brightness > 0.7) brightness = 0.7;
|
||||
if (brightness < 0.15) brightness = 0.15;
|
||||
call.set_brightness(brightness);
|
||||
} else if (mode == "Manual") {
|
||||
call.set_brightness(id(iaq_led_brightness).state);
|
||||
}
|
||||
|
||||
call.perform();
|
||||
|
||||
globals:
|
||||
- id: iaq_index
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
- id: computed_brightness
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.15'
|
||||
|
||||
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
connection_slots: 3
|
||||
|
||||
uart:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO18
|
||||
baud_rate: 9600
|
||||
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
rgb_order: GRB
|
||||
chipset: WS2812
|
||||
pin: GPIO16
|
||||
num_leds: 5
|
||||
name: "LED"
|
||||
id: led
|
||||
icon: mdi:led-on
|
||||
default_transition_length: 0s
|
||||
disabled_by_default: False
|
||||
effects:
|
||||
- pulse:
|
||||
name: "Breathing"
|
||||
min_brightness: 0.15
|
||||
max_brightness: 0.45
|
||||
transition_length: 3000ms
|
||||
update_interval: 3000ms
|
||||
- addressable_scan:
|
||||
name: "Scanner"
|
||||
scan_width: 2
|
||||
move_interval: 100ms
|
||||
- flicker:
|
||||
name: "Alert Flash"
|
||||
alpha: 95%
|
||||
intensity: 1.5%
|
||||
- addressable_rainbow:
|
||||
name: "Rainbow Effect"
|
||||
speed: 10
|
||||
width: 50
|
||||
|
||||
i2c:
|
||||
sda: GPIO8
|
||||
scl: GPIO9
|
||||
scan: true
|
||||
id: bus_a
|
||||
frequency: 100kHz
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
name: "OLED Power"
|
||||
id: oled_power
|
||||
optimistic: true
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
turn_on_action:
|
||||
- logger.log: "OLED turned ON"
|
||||
turn_off_action:
|
||||
- logger.log: "OLED turned OFF"
|
||||
|
||||
select:
|
||||
- platform: template
|
||||
name: "LED Mode"
|
||||
id: led_mode
|
||||
options:
|
||||
- "Auto"
|
||||
- "Manual"
|
||||
- "Breathing"
|
||||
- "Scanner"
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
on_value:
|
||||
then:
|
||||
- script.execute: update_led_state
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
name: "IAQ LED Brightness"
|
||||
id: iaq_led_brightness
|
||||
min_value: 0.15
|
||||
max_value: 1.0
|
||||
step: 0.01
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
|
||||
sensor:
|
||||
- platform: uptime
|
||||
name: Uptime Sensor
|
||||
|
||||
- platform: homeassistant
|
||||
id: inside_temperature
|
||||
entity_id: $internal_temp_sensor
|
||||
internal: true
|
||||
|
||||
- platform: homeassistant
|
||||
id: outside_temperature
|
||||
entity_id: $outside_temp_sensor
|
||||
internal: true
|
||||
|
||||
- platform: homeassistant
|
||||
id: todays_forecast_high
|
||||
entity_id: $todays_forecast_high_entity
|
||||
internal: true
|
||||
|
||||
- platform: homeassistant
|
||||
id: todays_forecast_low
|
||||
entity_id: $todays_forecast_low_entity
|
||||
internal: true
|
||||
|
||||
- platform: pmsx003
|
||||
type: PMSX003
|
||||
pm_1_0:
|
||||
id: pm1
|
||||
name: "PM <1.0µm Concentration"
|
||||
pm_2_5:
|
||||
id: pm25
|
||||
name: "PM <2.5µm Concentration"
|
||||
pm_10_0:
|
||||
id: pm10
|
||||
name: "PM <10.0µm Concentration"
|
||||
|
||||
- platform: bme680
|
||||
temperature:
|
||||
name: "BME680 Temperature"
|
||||
id: bmetemp
|
||||
oversampling: 16x
|
||||
pressure:
|
||||
name: "BME680 Pressure"
|
||||
id: bmepressure
|
||||
humidity:
|
||||
name: "BME680 Humidity"
|
||||
id: bmehum
|
||||
gas_resistance:
|
||||
name: "BME680 Gas Resistance"
|
||||
id: bmegas
|
||||
address: 0x77
|
||||
update_interval: 10s
|
||||
|
||||
## CO²/VOC Sensor
|
||||
- platform: ccs811
|
||||
eco2:
|
||||
name: "CCS811 CO²"
|
||||
accuracy_decimals: 0
|
||||
id: eco2
|
||||
tvoc:
|
||||
name: "CCS811 T-VOC"
|
||||
accuracy_decimals: 0
|
||||
id: tvoc
|
||||
address: 0x5A
|
||||
update_interval: 10s
|
||||
temperature: bmetemp
|
||||
humidity: bmehum
|
||||
## After Calibration, Uncomment and change "baseline:"
|
||||
baseline: 0x9CB1
|
||||
|
||||
- platform: template
|
||||
name: "Humidity Sensor"
|
||||
id: humi
|
||||
unit_of_measurement: "%"
|
||||
accuracy_decimals: 1
|
||||
lambda: |-
|
||||
return id(bmehum).state;
|
||||
update_interval: 10s
|
||||
|
||||
- platform: wifi_signal
|
||||
name: AQ WiFi Signal
|
||||
update_interval: 60s
|
||||
|
||||
- platform: veml7700
|
||||
address: 0x10
|
||||
update_interval: 10s
|
||||
# short variant of sensor definition:
|
||||
ambient_light:
|
||||
name: "Ambient Light"
|
||||
id: ambient_light
|
||||
filters:
|
||||
- sliding_window_moving_average:
|
||||
window_size: 5
|
||||
send_every: 1
|
||||
|
||||
font:
|
||||
- file: "fonts/Roboto-Regular.ttf"
|
||||
id: robotto
|
||||
size: 10
|
||||
|
||||
- file: "fonts/Roboto-Regular.ttf"
|
||||
id: font2
|
||||
size: 12
|
||||
|
||||
- file: "fonts/Poppins-Regular.ttf"
|
||||
id: font1
|
||||
size: 10
|
||||
|
||||
- file: "fonts/Poppins-Regular.ttf"
|
||||
id: poppinslarger
|
||||
size: 12
|
||||
|
||||
- file: "fonts/Poppins-SemiBold.ttf"
|
||||
id: poppinsbold
|
||||
size: 10
|
||||
|
||||
- file: 'fonts/materialdesignicons-webfont.ttf'
|
||||
id: font3
|
||||
size: 18
|
||||
glyphs:
|
||||
- "\U000F13D5" #mdi:home-minus-outline
|
||||
|
||||
- file: 'fonts/materialdesignicons-webfont.ttf'
|
||||
id: font4
|
||||
size: 40
|
||||
glyphs:
|
||||
- "\U000F0594" #"clear-night"
|
||||
- "\U000F0590" #"cloudy"
|
||||
- "\U000F0591" #"fog"
|
||||
- "\U000F0592" #"hail"
|
||||
- "\U000F0593" #"lightning"
|
||||
- "\U000F067E" #"lightning-rainy"
|
||||
- "\U000F0595" #"partlycloudy"
|
||||
- "\U000F0596" #"pouring"
|
||||
- "\U000F0597" #"rainy"
|
||||
- "\U000F0598" #"snowy"
|
||||
- "\U000F067F" #"snowy-rainy"
|
||||
- "\U000F0599" #"sunny"
|
||||
- "\U000F059D" #"windy"
|
||||
- "\U000F059E" #"windy-variant"
|
||||
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
model: "SSD1306 128x64"
|
||||
address: 0x3C
|
||||
id: oled_display
|
||||
rotation: 0
|
||||
i2c_id: bus_a
|
||||
pages:
|
||||
- id: page_air_quality
|
||||
lambda: |-
|
||||
if (id(oled_power).state) { // only draw if display is ON
|
||||
{
|
||||
// Combine the final string first
|
||||
char full_text[64];
|
||||
|
||||
if (id(iaq_reading).has_state()) {
|
||||
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
|
||||
} else {
|
||||
sprintf(full_text, "Air Quality: Booting...");
|
||||
}
|
||||
|
||||
int x, y, w, h;
|
||||
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
|
||||
int center_x = (128 - w) / 2; // OLED is 128px wide
|
||||
|
||||
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
|
||||
}
|
||||
|
||||
// --- Temp + Humidity on same line ---
|
||||
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
|
||||
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
|
||||
|
||||
// --- Other sensor lines ---
|
||||
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(eco2).state);
|
||||
it.printf(0, 36, id(font1), "TVOC: %.0f ppb", id(tvoc).state);
|
||||
|
||||
if (!isnan(id(pm25).state)) {
|
||||
it.printf(0, 46, id(font1), "PM2.5: %.0f", id(pm25).state);
|
||||
} else {
|
||||
it.printf(0, 46, id(font1), "PM2.5: ---");
|
||||
}
|
||||
}
|
||||
|
||||
- id: page_environment
|
||||
lambda: |-
|
||||
if (id(oled_power).state) { // only draw if display is ON
|
||||
{
|
||||
// Combine the final string first
|
||||
char full_text[64];
|
||||
|
||||
if (id(iaq_reading).has_state()) {
|
||||
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
|
||||
} else {
|
||||
sprintf(full_text, "Air Quality: Booting...");
|
||||
}
|
||||
|
||||
int x, y, w, h;
|
||||
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
|
||||
int center_x = (128 - w) / 2; // OLED is 128px wide
|
||||
|
||||
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
|
||||
}
|
||||
// --- Temp + Humidity on same line ---
|
||||
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
|
||||
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
|
||||
// --- Other sensor lines ---
|
||||
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(ambient_light).state);
|
||||
}
|
||||
|
||||
- id: page_weather
|
||||
lambda: |-
|
||||
if (id(weather_state).has_state()) {
|
||||
std::map<std::string, std::string> weather_icon_map
|
||||
{
|
||||
{"clear-night", "\U000F0594"},
|
||||
{"cloudy", "\U000F0590"},
|
||||
{"fog", "\U000F0591"},
|
||||
{"hail", "\U000F0592"},
|
||||
{"lightning", "\U000F0593"},
|
||||
{"lightning-rainy", "\U000F067E"},
|
||||
{"partlycloudy", "\U000F0595"},
|
||||
{"pouring", "\U000F0596"},
|
||||
{"rainy", "\U000F0597"},
|
||||
{"snowy", "\U000F0598"},
|
||||
{"snowy-rainy", "\U000F067F"},
|
||||
{"sunny", "\U000F0599"},
|
||||
{"windy", "\U000F059D"},
|
||||
{"windy-variant", "\U000F059E"},
|
||||
};
|
||||
it.printf(0, it.get_height(), id(font4), TextAlign::BASELINE_LEFT, weather_icon_map[id(weather_state).state.c_str()].c_str());
|
||||
}
|
||||
|
||||
// Print time in HH:MM format
|
||||
it.strftime(0, 0, id(font1), TextAlign::TOP_LEFT, "%H:%M %a", id(homeassistant_time).now());
|
||||
|
||||
//Print day of week
|
||||
//it.strftime(40, 0, id(font1), TextAlign::TOP_LEFT, "%a", id(homeassistant_time).now());
|
||||
|
||||
it.line(0, 20, it.get_width(), 20);
|
||||
|
||||
//Print home icon
|
||||
//it.printf(70, 0, id(font3), "\U000F13D5");
|
||||
|
||||
// Print inside temperature (from homeassistant sensor)
|
||||
if (id(inside_temperature).has_state()) {
|
||||
it.printf(it.get_width(), 0, id(font1), TextAlign::TOP_RIGHT , "%7.1f°", id(inside_temperature).state);
|
||||
}
|
||||
|
||||
// Print outside temperature (from homeassistant sensor)
|
||||
if (id(outside_temperature).has_state()) {
|
||||
it.printf(42, 32, id(font2), "%.1f°", id(outside_temperature).state);
|
||||
}
|
||||
|
||||
// Print Forecast High
|
||||
if (id(todays_forecast_high).has_state()) {
|
||||
it.printf(it.get_width(), 32, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_high).state);
|
||||
}
|
||||
|
||||
// Print Forecast Low
|
||||
if (id(todays_forecast_low).has_state()) {
|
||||
it.printf(it.get_width(), 48, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_low).state);
|
||||
}
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: weather_state
|
||||
name: "Current Weather Icon"
|
||||
entity_id: $weather_entity
|
||||
internal: true
|
||||
|
||||
- platform: template
|
||||
name: "PM 2.5 Air Quality"
|
||||
icon: mdi:air-filter
|
||||
id: aq_reading
|
||||
lambda: |-
|
||||
if (id(pm25).state <= 12) {
|
||||
return {"Good"};
|
||||
}
|
||||
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
|
||||
return {"Moderate"};
|
||||
}
|
||||
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
|
||||
return {"Unhealthy(SG)"};
|
||||
}
|
||||
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
|
||||
return {"Unhealthy"};
|
||||
}
|
||||
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
|
||||
return {"Very Unhealthy"};
|
||||
}
|
||||
else if (id(pm25).state >= 250.5) {
|
||||
return {"Hazardous"};
|
||||
}
|
||||
return {};
|
||||
update_interval: 30s
|
||||
|
||||
- platform: template
|
||||
name: "PM 10 Air Quality"
|
||||
icon: mdi:air-filter
|
||||
id: aq_10_reading
|
||||
lambda: |-
|
||||
if (id(pm10).state <= 54) {
|
||||
return {"Good"};
|
||||
}
|
||||
else if ((id(pm10).state >= 55) && (id(pm10).state <= 154)) {
|
||||
return {"Moderate"};
|
||||
}
|
||||
else if ((id(pm10).state >= 155) && (id(pm10).state <= 254)) {
|
||||
return {"Unhealthy(SG)"};
|
||||
}
|
||||
else if ((id(pm10).state >= 255) && (id(pm10).state <= 354)) {
|
||||
return {"Unhealthy"};
|
||||
}
|
||||
else if ((id(pm10).state >= 355) && (id(pm10).state <= 424)) {
|
||||
return {"Very Unhealthy"};
|
||||
}
|
||||
else if (id(pm10).state >= 425) {
|
||||
return {"Hazardous"};
|
||||
}
|
||||
return {};
|
||||
update_interval: 30s
|
||||
|
||||
- platform: template
|
||||
name: "Livingroom IAQ"
|
||||
icon: "mdi:air-filter"
|
||||
id: iaq_reading
|
||||
lambda: |-
|
||||
// 1. Safety Check: Ensure all sensors have valid numbers
|
||||
if (isnan(id(humi).state) || isnan(id(pm25).state) || isnan(id(eco2).state) || isnan(id(tvoc).state)) {
|
||||
return {"Booting..."};
|
||||
}
|
||||
id(iaq_index) = 0;
|
||||
|
||||
if (id(humi).state < 10 or id(humi).state > 90) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
else if (id(humi).state < 20 or id(humi).state > 80) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(humi).state < 30 or id(humi).state > 70) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(humi).state < 40 or id(humi).state > 60) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(humi).state >= 40 and id(humi).state <= 60) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
|
||||
if (id(pm25).state <= 12) {
|
||||
id(iaq_index) += 6;
|
||||
}
|
||||
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(pm25).state >= 250.5) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(eco2).state <= 600) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(eco2).state <= 800) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(eco2).state <= 1500) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(eco2).state <= 1800) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(eco2).state > 1800) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(tvoc).state <= 65) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(tvoc).state <= 220) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(tvoc).state <= 660) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(tvoc).state <= 2200) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(tvoc).state > 2200) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
|
||||
|
||||
if (id(iaq_index) <= 11) {
|
||||
return {"Unhealthy"};
|
||||
}
|
||||
else if (id(iaq_index) <= 14) {
|
||||
return {"Poor"};
|
||||
}
|
||||
else if (id(iaq_index) <= 17) {
|
||||
return {"Moderate"};
|
||||
}
|
||||
else if (id(iaq_index) <= 19) {
|
||||
return {"Good"};
|
||||
}
|
||||
else if (id(iaq_index) > 19) {
|
||||
return {"Excellent"};
|
||||
}
|
||||
|
||||
return {};
|
||||
update_interval: 10s
|
||||
on_value:
|
||||
then:
|
||||
- script.execute: update_led_color
|
||||
|
||||
- platform: template
|
||||
name: "Livingroom IAQ Calculation"
|
||||
icon: "mdi:air-filter"
|
||||
id: iaq_reading_calculation
|
||||
lambda: |-
|
||||
id(iaq_index) = 0;
|
||||
|
||||
if (id(humi).state < 10 or id(humi).state > 90) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
else if (id(humi).state < 20 or id(humi).state > 80) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(humi).state < 30 or id(humi).state > 70) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(humi).state < 40 or id(humi).state > 60) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(humi).state >= 40 and id(humi).state <= 60) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
|
||||
if (id(pm25).state <= 12) {
|
||||
id(iaq_index) += 6;
|
||||
}
|
||||
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(pm25).state >= 250.5) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(eco2).state <= 600) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(eco2).state <= 800) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(eco2).state <= 1500) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(eco2).state <= 1800) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(eco2).state > 1800) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(tvoc).state <= 65) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(tvoc).state <= 220) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(tvoc).state <= 660) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(tvoc).state <= 2200) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(tvoc).state > 2200) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
|
||||
return std::to_string(id(iaq_index));
|
||||
|
||||
update_interval: 10s
|
||||
776
esphome/airqualitysensor-2.yaml
Normal file
776
esphome/airqualitysensor-2.yaml
Normal file
@@ -0,0 +1,776 @@
|
||||
substitutions: #substitute your own values in this section
|
||||
internal_temp_sensor: sensor.meat_heater_current_temperature #entity from Home Assistant
|
||||
outside_temp_sensor: sensor.home_realfeel_temperature #entity from Home Assistant
|
||||
weather_entity: weather.home #entity from Home Assistant
|
||||
todays_forecast_high_entity: sensor.home_realfeel_temperature_max_day_0 #entity from Home Assistant
|
||||
todays_forecast_low_entity: sensor.home_realfeel_temperature_min_day_0 #entity from Home Assistant
|
||||
|
||||
esphome:
|
||||
name: airqualitysensor-2
|
||||
friendly_name: AirQualitySensor-2
|
||||
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- light.turn_on:
|
||||
id: led
|
||||
brightness: 0.4
|
||||
effect: "Rainbow Effect"
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
cpu_frequency: 240MHz
|
||||
variant: esp32s3
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: "JPYycQ6Uxuubxrfu66M/xAyUPGEl2DiJBCbpCCs9w/4="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "42087a27f07378b5499426cf544a6fa6"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "Airqualitysensor-2"
|
||||
password: "uhEm8rcU8sMF"
|
||||
|
||||
captive_portal:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: homeassistant_time
|
||||
|
||||
on_time:
|
||||
- seconds: 0
|
||||
minutes: 0
|
||||
hours: 6 # 6 AM
|
||||
then:
|
||||
- logger.log: "Automatic morning turn ON OLED"
|
||||
- switch.turn_on: oled_power
|
||||
|
||||
- seconds: 0
|
||||
minutes: 0
|
||||
hours: 19 # 7 PM
|
||||
then:
|
||||
- logger.log: "Automatic evening turn OFF OLED"
|
||||
- switch.turn_off: oled_power
|
||||
|
||||
script:
|
||||
# SCRIPT 1: Runs when the mode is changed from the select
|
||||
- id: update_led_state
|
||||
then:
|
||||
- lambda: |-
|
||||
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
|
||||
return; // Exit script, leave rainbow alone
|
||||
}
|
||||
|
||||
std::string quality = id(iaq_reading).state;
|
||||
float lux = id(ambient_light).state;
|
||||
std::string mode = id(led_mode).state;
|
||||
|
||||
float brightness = 0.15;
|
||||
float r = 0.0, g = 0.0, b = 0.0;
|
||||
std::string selected_effect = "None";
|
||||
|
||||
// --- 1. Determine desired color ---
|
||||
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
|
||||
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
|
||||
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
|
||||
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
|
||||
else if (quality == "Unhealthy") {
|
||||
r = 1.0; g = 0.0; b = 1.0;
|
||||
selected_effect = "Alert Flash"; // Alert overrides mode
|
||||
}
|
||||
|
||||
// --- 2. Determine desired effect based on mode (if not alerting) ---
|
||||
if (selected_effect != "Alert Flash") {
|
||||
if (mode == "Breathing") {
|
||||
selected_effect = "Breathing";
|
||||
} else if (mode == "Scanner") {
|
||||
selected_effect = "Scanner";
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Apply changes ---
|
||||
auto call = id(led).turn_on();
|
||||
call.set_rgb(r, g, b); // Always set color
|
||||
|
||||
if (selected_effect == "None") {
|
||||
// We want a solid light (Auto/Manual)
|
||||
if (mode == "Auto") {
|
||||
if (isnan(lux)) lux = 0;
|
||||
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
|
||||
if (brightness > 0.7) brightness = 0.7;
|
||||
if (brightness < 0.15) brightness = 0.15;
|
||||
} else { // Manual
|
||||
brightness = id(iaq_led_brightness).state;
|
||||
}
|
||||
call.set_brightness(brightness);
|
||||
}
|
||||
|
||||
// This script *always* sets the effect
|
||||
call.set_effect(selected_effect);
|
||||
call.perform();
|
||||
|
||||
# SCRIPT 2: Runs when IAQ *value* changes (every 10s)
|
||||
- id: update_led_color
|
||||
then:
|
||||
- lambda: |-
|
||||
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
|
||||
return; // Exit script, leave rainbow alone
|
||||
}
|
||||
|
||||
std::string quality = id(iaq_reading).state;
|
||||
std::string mode = id(led_mode).state;
|
||||
float lux = id(ambient_light).state;
|
||||
|
||||
float r = 0.0, g = 0.0, b = 0.0;
|
||||
|
||||
// --- 1. Determine desired color ---
|
||||
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
|
||||
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
|
||||
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
|
||||
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
|
||||
else if (quality == "Unhealthy") {
|
||||
r = 1.0; g = 0.0; b = 1.0;
|
||||
// Force Alert Flash and EXIT
|
||||
id(led).turn_on().set_rgb(r, g, b).set_effect("Alert Flash").perform();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 2. If we are here, AQ is OK. Reset Effect! ---
|
||||
auto call = id(led).turn_on();
|
||||
call.set_rgb(r, g, b);
|
||||
|
||||
// Determine which effect to restore based on the Select Mode
|
||||
std::string restore_effect = "None";
|
||||
if (mode == "Breathing") restore_effect = "Breathing";
|
||||
else if (mode == "Scanner") restore_effect = "Scanner";
|
||||
|
||||
// Explicitly set the effect (This stops the Alert Flash)
|
||||
call.set_effect(restore_effect);
|
||||
|
||||
// --- 3. Apply Brightness ---
|
||||
if (mode == "Auto") {
|
||||
float brightness = 0.15;
|
||||
if (isnan(lux)) lux = 0;
|
||||
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
|
||||
if (brightness > 0.7) brightness = 0.7;
|
||||
if (brightness < 0.15) brightness = 0.15;
|
||||
call.set_brightness(brightness);
|
||||
} else if (mode == "Manual") {
|
||||
call.set_brightness(id(iaq_led_brightness).state);
|
||||
}
|
||||
|
||||
call.perform();
|
||||
|
||||
globals:
|
||||
- id: iaq_index
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
- id: computed_brightness
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.15'
|
||||
|
||||
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
connection_slots: 3
|
||||
|
||||
uart:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO18
|
||||
baud_rate: 9600
|
||||
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
rgb_order: GRB
|
||||
chipset: WS2812
|
||||
pin: GPIO16
|
||||
num_leds: 5
|
||||
name: "LED"
|
||||
id: led
|
||||
icon: mdi:led-on
|
||||
default_transition_length: 0s
|
||||
disabled_by_default: False
|
||||
effects:
|
||||
- pulse:
|
||||
name: "Breathing"
|
||||
min_brightness: 0.15
|
||||
max_brightness: 0.45
|
||||
transition_length: 3000ms
|
||||
update_interval: 3000ms
|
||||
- addressable_scan:
|
||||
name: "Scanner"
|
||||
scan_width: 2
|
||||
move_interval: 100ms
|
||||
- flicker:
|
||||
name: "Alert Flash"
|
||||
alpha: 95%
|
||||
intensity: 1.5%
|
||||
- addressable_rainbow:
|
||||
name: "Rainbow Effect"
|
||||
speed: 10
|
||||
width: 50
|
||||
|
||||
i2c:
|
||||
sda: GPIO8
|
||||
scl: GPIO9
|
||||
scan: true
|
||||
id: bus_a
|
||||
frequency: 100kHz
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
name: "OLED Power"
|
||||
id: oled_power
|
||||
optimistic: true
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
turn_on_action:
|
||||
- logger.log: "OLED turned ON"
|
||||
turn_off_action:
|
||||
- logger.log: "OLED turned OFF"
|
||||
|
||||
select:
|
||||
- platform: template
|
||||
name: "LED Mode"
|
||||
id: led_mode
|
||||
options:
|
||||
- "Auto"
|
||||
- "Manual"
|
||||
- "Breathing"
|
||||
- "Scanner"
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
on_value:
|
||||
then:
|
||||
- script.execute: update_led_state
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
name: "IAQ LED Brightness"
|
||||
id: iaq_led_brightness
|
||||
min_value: 0.15
|
||||
max_value: 1.0
|
||||
step: 0.01
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
|
||||
sensor:
|
||||
- platform: uptime
|
||||
name: Uptime Sensor
|
||||
|
||||
- platform: homeassistant
|
||||
id: inside_temperature
|
||||
entity_id: $internal_temp_sensor
|
||||
internal: true
|
||||
|
||||
- platform: homeassistant
|
||||
id: outside_temperature
|
||||
entity_id: $outside_temp_sensor
|
||||
internal: true
|
||||
|
||||
- platform: homeassistant
|
||||
id: todays_forecast_high
|
||||
entity_id: $todays_forecast_high_entity
|
||||
internal: true
|
||||
|
||||
- platform: homeassistant
|
||||
id: todays_forecast_low
|
||||
entity_id: $todays_forecast_low_entity
|
||||
internal: true
|
||||
|
||||
- platform: pmsx003
|
||||
type: PMSX003
|
||||
pm_1_0:
|
||||
id: pm1
|
||||
name: "PM <1.0µm Concentration"
|
||||
pm_2_5:
|
||||
id: pm25
|
||||
name: "PM <2.5µm Concentration"
|
||||
pm_10_0:
|
||||
id: pm10
|
||||
name: "PM <10.0µm Concentration"
|
||||
|
||||
- platform: bme680
|
||||
temperature:
|
||||
name: "BME680 Temperature"
|
||||
id: bmetemp
|
||||
oversampling: 16x
|
||||
pressure:
|
||||
name: "BME680 Pressure"
|
||||
id: bmepressure
|
||||
humidity:
|
||||
name: "BME680 Humidity"
|
||||
id: bmehum
|
||||
gas_resistance:
|
||||
name: "BME680 Gas Resistance"
|
||||
id: bmegas
|
||||
address: 0x77
|
||||
update_interval: 10s
|
||||
|
||||
## CO²/VOC Sensor
|
||||
- platform: ccs811
|
||||
eco2:
|
||||
name: "CCS811 CO²"
|
||||
accuracy_decimals: 0
|
||||
id: eco2
|
||||
tvoc:
|
||||
name: "CCS811 T-VOC"
|
||||
accuracy_decimals: 0
|
||||
id: tvoc
|
||||
address: 0x5A
|
||||
update_interval: 10s
|
||||
temperature: bmetemp
|
||||
humidity: bmehum
|
||||
## After Calibration, Uncomment and change "baseline:"
|
||||
baseline: 0x9CB1
|
||||
|
||||
- platform: template
|
||||
name: "Humidity Sensor"
|
||||
id: humi
|
||||
unit_of_measurement: "%"
|
||||
accuracy_decimals: 1
|
||||
lambda: |-
|
||||
return id(bmehum).state;
|
||||
update_interval: 10s
|
||||
|
||||
- platform: wifi_signal
|
||||
name: AQ WiFi Signal
|
||||
update_interval: 60s
|
||||
|
||||
- platform: veml7700
|
||||
address: 0x10
|
||||
update_interval: 10s
|
||||
# short variant of sensor definition:
|
||||
ambient_light:
|
||||
name: "Ambient Light"
|
||||
id: ambient_light
|
||||
filters:
|
||||
- sliding_window_moving_average:
|
||||
window_size: 5
|
||||
send_every: 1
|
||||
|
||||
font:
|
||||
- file: "fonts/Roboto-Regular.ttf"
|
||||
id: robotto
|
||||
size: 10
|
||||
|
||||
- file: "fonts/Roboto-Regular.ttf"
|
||||
id: font2
|
||||
size: 12
|
||||
|
||||
- file: "fonts/Poppins-Regular.ttf"
|
||||
id: font1
|
||||
size: 10
|
||||
|
||||
- file: "fonts/Poppins-Regular.ttf"
|
||||
id: poppinslarger
|
||||
size: 12
|
||||
|
||||
- file: "fonts/Poppins-SemiBold.ttf"
|
||||
id: poppinsbold
|
||||
size: 10
|
||||
|
||||
- file: 'fonts/materialdesignicons-webfont.ttf'
|
||||
id: font3
|
||||
size: 18
|
||||
glyphs:
|
||||
- "\U000F13D5" #mdi:home-minus-outline
|
||||
|
||||
- file: 'fonts/materialdesignicons-webfont.ttf'
|
||||
id: font4
|
||||
size: 40
|
||||
glyphs:
|
||||
- "\U000F0594" #"clear-night"
|
||||
- "\U000F0590" #"cloudy"
|
||||
- "\U000F0591" #"fog"
|
||||
- "\U000F0592" #"hail"
|
||||
- "\U000F0593" #"lightning"
|
||||
- "\U000F067E" #"lightning-rainy"
|
||||
- "\U000F0595" #"partlycloudy"
|
||||
- "\U000F0596" #"pouring"
|
||||
- "\U000F0597" #"rainy"
|
||||
- "\U000F0598" #"snowy"
|
||||
- "\U000F067F" #"snowy-rainy"
|
||||
- "\U000F0599" #"sunny"
|
||||
- "\U000F059D" #"windy"
|
||||
- "\U000F059E" #"windy-variant"
|
||||
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
model: "SSD1306 128x64"
|
||||
address: 0x3C
|
||||
id: oled_display
|
||||
rotation: 0
|
||||
i2c_id: bus_a
|
||||
pages:
|
||||
- id: page_air_quality
|
||||
lambda: |-
|
||||
if (id(oled_power).state) { // only draw if display is ON
|
||||
{
|
||||
// Combine the final string first
|
||||
char full_text[64];
|
||||
|
||||
if (id(iaq_reading).has_state()) {
|
||||
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
|
||||
} else {
|
||||
sprintf(full_text, "Air Quality: Booting...");
|
||||
}
|
||||
|
||||
int x, y, w, h;
|
||||
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
|
||||
int center_x = (128 - w) / 2; // OLED is 128px wide
|
||||
|
||||
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
|
||||
}
|
||||
|
||||
// --- Temp + Humidity on same line ---
|
||||
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
|
||||
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
|
||||
|
||||
// --- Other sensor lines ---
|
||||
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(eco2).state);
|
||||
it.printf(0, 36, id(font1), "TVOC: %.0f ppb", id(tvoc).state);
|
||||
|
||||
if (!isnan(id(pm25).state)) {
|
||||
it.printf(0, 46, id(font1), "PM2.5: %.0f", id(pm25).state);
|
||||
} else {
|
||||
it.printf(0, 46, id(font1), "PM2.5: ---");
|
||||
}
|
||||
}
|
||||
|
||||
- id: page_environment
|
||||
lambda: |-
|
||||
if (id(oled_power).state) { // only draw if display is ON
|
||||
{
|
||||
// Combine the final string first
|
||||
char full_text[64];
|
||||
|
||||
if (id(iaq_reading).has_state()) {
|
||||
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
|
||||
} else {
|
||||
sprintf(full_text, "Air Quality: Booting...");
|
||||
}
|
||||
|
||||
int x, y, w, h;
|
||||
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
|
||||
int center_x = (128 - w) / 2; // OLED is 128px wide
|
||||
|
||||
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
|
||||
}
|
||||
// --- Temp + Humidity on same line ---
|
||||
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
|
||||
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
|
||||
// --- Other sensor lines ---
|
||||
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(ambient_light).state);
|
||||
}
|
||||
|
||||
- id: page_weather
|
||||
lambda: |-
|
||||
if (id(weather_state).has_state()) {
|
||||
std::map<std::string, std::string> weather_icon_map
|
||||
{
|
||||
{"clear-night", "\U000F0594"},
|
||||
{"cloudy", "\U000F0590"},
|
||||
{"fog", "\U000F0591"},
|
||||
{"hail", "\U000F0592"},
|
||||
{"lightning", "\U000F0593"},
|
||||
{"lightning-rainy", "\U000F067E"},
|
||||
{"partlycloudy", "\U000F0595"},
|
||||
{"pouring", "\U000F0596"},
|
||||
{"rainy", "\U000F0597"},
|
||||
{"snowy", "\U000F0598"},
|
||||
{"snowy-rainy", "\U000F067F"},
|
||||
{"sunny", "\U000F0599"},
|
||||
{"windy", "\U000F059D"},
|
||||
{"windy-variant", "\U000F059E"},
|
||||
};
|
||||
it.printf(0, it.get_height(), id(font4), TextAlign::BASELINE_LEFT, weather_icon_map[id(weather_state).state.c_str()].c_str());
|
||||
}
|
||||
|
||||
// Print time in HH:MM format
|
||||
it.strftime(0, 0, id(font1), TextAlign::TOP_LEFT, "%H:%M %a", id(homeassistant_time).now());
|
||||
|
||||
//Print day of week
|
||||
//it.strftime(40, 0, id(font1), TextAlign::TOP_LEFT, "%a", id(homeassistant_time).now());
|
||||
|
||||
it.line(0, 20, it.get_width(), 20);
|
||||
|
||||
//Print home icon
|
||||
//it.printf(70, 0, id(font3), "\U000F13D5");
|
||||
|
||||
// Print inside temperature (from homeassistant sensor)
|
||||
if (id(inside_temperature).has_state()) {
|
||||
it.printf(it.get_width(), 0, id(font1), TextAlign::TOP_RIGHT , "%7.1f°", id(inside_temperature).state);
|
||||
}
|
||||
|
||||
// Print outside temperature (from homeassistant sensor)
|
||||
if (id(outside_temperature).has_state()) {
|
||||
it.printf(42, 32, id(font2), "%.1f°", id(outside_temperature).state);
|
||||
}
|
||||
|
||||
// Print Forecast High
|
||||
if (id(todays_forecast_high).has_state()) {
|
||||
it.printf(it.get_width(), 32, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_high).state);
|
||||
}
|
||||
|
||||
// Print Forecast Low
|
||||
if (id(todays_forecast_low).has_state()) {
|
||||
it.printf(it.get_width(), 48, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_low).state);
|
||||
}
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: weather_state
|
||||
name: "Current Weather Icon"
|
||||
entity_id: $weather_entity
|
||||
internal: true
|
||||
|
||||
- platform: template
|
||||
name: "PM 2.5 Air Quality"
|
||||
icon: mdi:air-filter
|
||||
id: aq_reading
|
||||
lambda: |-
|
||||
if (id(pm25).state <= 12) {
|
||||
return {"Good"};
|
||||
}
|
||||
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
|
||||
return {"Moderate"};
|
||||
}
|
||||
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
|
||||
return {"Unhealthy(SG)"};
|
||||
}
|
||||
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
|
||||
return {"Unhealthy"};
|
||||
}
|
||||
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
|
||||
return {"Very Unhealthy"};
|
||||
}
|
||||
else if (id(pm25).state >= 250.5) {
|
||||
return {"Hazardous"};
|
||||
}
|
||||
return {};
|
||||
update_interval: 30s
|
||||
|
||||
- platform: template
|
||||
name: "PM 10 Air Quality"
|
||||
icon: mdi:air-filter
|
||||
id: aq_10_reading
|
||||
lambda: |-
|
||||
if (id(pm10).state <= 54) {
|
||||
return {"Good"};
|
||||
}
|
||||
else if ((id(pm10).state >= 55) && (id(pm10).state <= 154)) {
|
||||
return {"Moderate"};
|
||||
}
|
||||
else if ((id(pm10).state >= 155) && (id(pm10).state <= 254)) {
|
||||
return {"Unhealthy(SG)"};
|
||||
}
|
||||
else if ((id(pm10).state >= 255) && (id(pm10).state <= 354)) {
|
||||
return {"Unhealthy"};
|
||||
}
|
||||
else if ((id(pm10).state >= 355) && (id(pm10).state <= 424)) {
|
||||
return {"Very Unhealthy"};
|
||||
}
|
||||
else if (id(pm10).state >= 425) {
|
||||
return {"Hazardous"};
|
||||
}
|
||||
return {};
|
||||
update_interval: 30s
|
||||
|
||||
- platform: template
|
||||
name: "Livingroom IAQ"
|
||||
icon: "mdi:air-filter"
|
||||
id: iaq_reading
|
||||
lambda: |-
|
||||
// 1. Safety Check: Ensure all sensors have valid numbers
|
||||
if (isnan(id(humi).state) || isnan(id(pm25).state) || isnan(id(eco2).state) || isnan(id(tvoc).state)) {
|
||||
return {"Booting..."};
|
||||
}
|
||||
id(iaq_index) = 0;
|
||||
|
||||
if (id(humi).state < 10 or id(humi).state > 90) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
else if (id(humi).state < 20 or id(humi).state > 80) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(humi).state < 30 or id(humi).state > 70) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(humi).state < 40 or id(humi).state > 60) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(humi).state >= 40 and id(humi).state <= 60) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
|
||||
if (id(pm25).state <= 12) {
|
||||
id(iaq_index) += 6;
|
||||
}
|
||||
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(pm25).state >= 250.5) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(eco2).state <= 600) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(eco2).state <= 800) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(eco2).state <= 1500) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(eco2).state <= 1800) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(eco2).state > 1800) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(tvoc).state <= 65) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(tvoc).state <= 220) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(tvoc).state <= 660) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(tvoc).state <= 2200) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(tvoc).state > 2200) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
|
||||
|
||||
if (id(iaq_index) <= 11) {
|
||||
return {"Unhealthy"};
|
||||
}
|
||||
else if (id(iaq_index) <= 14) {
|
||||
return {"Poor"};
|
||||
}
|
||||
else if (id(iaq_index) <= 17) {
|
||||
return {"Moderate"};
|
||||
}
|
||||
else if (id(iaq_index) <= 19) {
|
||||
return {"Good"};
|
||||
}
|
||||
else if (id(iaq_index) > 19) {
|
||||
return {"Excellent"};
|
||||
}
|
||||
|
||||
return {};
|
||||
update_interval: 10s
|
||||
on_value:
|
||||
then:
|
||||
- script.execute: update_led_color
|
||||
|
||||
- platform: template
|
||||
name: "Livingroom IAQ Calculation"
|
||||
icon: "mdi:air-filter"
|
||||
id: iaq_reading_calculation
|
||||
lambda: |-
|
||||
id(iaq_index) = 0;
|
||||
|
||||
if (id(humi).state < 10 or id(humi).state > 90) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
else if (id(humi).state < 20 or id(humi).state > 80) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(humi).state < 30 or id(humi).state > 70) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(humi).state < 40 or id(humi).state > 60) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(humi).state >= 40 and id(humi).state <= 60) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
|
||||
if (id(pm25).state <= 12) {
|
||||
id(iaq_index) += 6;
|
||||
}
|
||||
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(pm25).state >= 250.5) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(eco2).state <= 600) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(eco2).state <= 800) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(eco2).state <= 1500) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(eco2).state <= 1800) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(eco2).state > 1800) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(tvoc).state <= 65) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(tvoc).state <= 220) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(tvoc).state <= 660) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(tvoc).state <= 2200) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(tvoc).state > 2200) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
|
||||
return std::to_string(id(iaq_index));
|
||||
|
||||
update_interval: 10s
|
||||
776
esphome/airqualitysensor-3.yaml
Normal file
776
esphome/airqualitysensor-3.yaml
Normal file
@@ -0,0 +1,776 @@
|
||||
substitutions: #substitute your own values in this section
|
||||
internal_temp_sensor: sensor.meat_heater_current_temperature #entity from Home Assistant
|
||||
outside_temp_sensor: sensor.home_realfeel_temperature #entity from Home Assistant
|
||||
weather_entity: weather.home #entity from Home Assistant
|
||||
todays_forecast_high_entity: sensor.home_realfeel_temperature_max_day_0 #entity from Home Assistant
|
||||
todays_forecast_low_entity: sensor.home_realfeel_temperature_min_day_0 #entity from Home Assistant
|
||||
|
||||
esphome:
|
||||
name: airqualitysensor-3
|
||||
friendly_name: AirQualitySensor-3
|
||||
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- light.turn_on:
|
||||
id: led
|
||||
brightness: 0.4
|
||||
effect: "Rainbow Effect"
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
cpu_frequency: 240MHz
|
||||
variant: esp32s3
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: "p7SGIY1i+hwzZCQMliScoY2B98Qva1nPGUnzTr+8knE="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "81bd35b0916ed26675b611274e652689"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "Airqualitysensor-3"
|
||||
password: "a8CsGahAGljj"
|
||||
|
||||
captive_portal:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: homeassistant_time
|
||||
|
||||
on_time:
|
||||
- seconds: 0
|
||||
minutes: 0
|
||||
hours: 6 # 6 AM
|
||||
then:
|
||||
- logger.log: "Automatic morning turn ON OLED"
|
||||
- switch.turn_on: oled_power
|
||||
|
||||
- seconds: 0
|
||||
minutes: 0
|
||||
hours: 19 # 7 PM
|
||||
then:
|
||||
- logger.log: "Automatic evening turn OFF OLED"
|
||||
- switch.turn_off: oled_power
|
||||
|
||||
script:
|
||||
# SCRIPT 1: Runs when the mode is changed from the select
|
||||
- id: update_led_state
|
||||
then:
|
||||
- lambda: |-
|
||||
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
|
||||
return; // Exit script, leave rainbow alone
|
||||
}
|
||||
|
||||
std::string quality = id(iaq_reading).state;
|
||||
float lux = id(ambient_light).state;
|
||||
std::string mode = id(led_mode).state;
|
||||
|
||||
float brightness = 0.15;
|
||||
float r = 0.0, g = 0.0, b = 0.0;
|
||||
std::string selected_effect = "None";
|
||||
|
||||
// --- 1. Determine desired color ---
|
||||
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
|
||||
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
|
||||
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
|
||||
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
|
||||
else if (quality == "Unhealthy") {
|
||||
r = 1.0; g = 0.0; b = 1.0;
|
||||
selected_effect = "Alert Flash"; // Alert overrides mode
|
||||
}
|
||||
|
||||
// --- 2. Determine desired effect based on mode (if not alerting) ---
|
||||
if (selected_effect != "Alert Flash") {
|
||||
if (mode == "Breathing") {
|
||||
selected_effect = "Breathing";
|
||||
} else if (mode == "Scanner") {
|
||||
selected_effect = "Scanner";
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Apply changes ---
|
||||
auto call = id(led).turn_on();
|
||||
call.set_rgb(r, g, b); // Always set color
|
||||
|
||||
if (selected_effect == "None") {
|
||||
// We want a solid light (Auto/Manual)
|
||||
if (mode == "Auto") {
|
||||
if (isnan(lux)) lux = 0;
|
||||
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
|
||||
if (brightness > 0.7) brightness = 0.7;
|
||||
if (brightness < 0.15) brightness = 0.15;
|
||||
} else { // Manual
|
||||
brightness = id(iaq_led_brightness).state;
|
||||
}
|
||||
call.set_brightness(brightness);
|
||||
}
|
||||
|
||||
// This script *always* sets the effect
|
||||
call.set_effect(selected_effect);
|
||||
call.perform();
|
||||
|
||||
# SCRIPT 2: Runs when IAQ *value* changes (every 10s)
|
||||
- id: update_led_color
|
||||
then:
|
||||
- lambda: |-
|
||||
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
|
||||
return; // Exit script, leave rainbow alone
|
||||
}
|
||||
|
||||
std::string quality = id(iaq_reading).state;
|
||||
std::string mode = id(led_mode).state;
|
||||
float lux = id(ambient_light).state;
|
||||
|
||||
float r = 0.0, g = 0.0, b = 0.0;
|
||||
|
||||
// --- 1. Determine desired color ---
|
||||
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
|
||||
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
|
||||
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
|
||||
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
|
||||
else if (quality == "Unhealthy") {
|
||||
r = 1.0; g = 0.0; b = 1.0;
|
||||
// Force Alert Flash and EXIT
|
||||
id(led).turn_on().set_rgb(r, g, b).set_effect("Alert Flash").perform();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 2. If we are here, AQ is OK. Reset Effect! ---
|
||||
auto call = id(led).turn_on();
|
||||
call.set_rgb(r, g, b);
|
||||
|
||||
// Determine which effect to restore based on the Select Mode
|
||||
std::string restore_effect = "None";
|
||||
if (mode == "Breathing") restore_effect = "Breathing";
|
||||
else if (mode == "Scanner") restore_effect = "Scanner";
|
||||
|
||||
// Explicitly set the effect (This stops the Alert Flash)
|
||||
call.set_effect(restore_effect);
|
||||
|
||||
// --- 3. Apply Brightness ---
|
||||
if (mode == "Auto") {
|
||||
float brightness = 0.15;
|
||||
if (isnan(lux)) lux = 0;
|
||||
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
|
||||
if (brightness > 0.7) brightness = 0.7;
|
||||
if (brightness < 0.15) brightness = 0.15;
|
||||
call.set_brightness(brightness);
|
||||
} else if (mode == "Manual") {
|
||||
call.set_brightness(id(iaq_led_brightness).state);
|
||||
}
|
||||
|
||||
call.perform();
|
||||
|
||||
globals:
|
||||
- id: iaq_index
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
- id: computed_brightness
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.15'
|
||||
|
||||
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
connection_slots: 3
|
||||
|
||||
uart:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO18
|
||||
baud_rate: 9600
|
||||
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
rgb_order: GRB
|
||||
chipset: WS2812
|
||||
pin: GPIO16
|
||||
num_leds: 5
|
||||
name: "LED"
|
||||
id: led
|
||||
icon: mdi:led-on
|
||||
default_transition_length: 0s
|
||||
disabled_by_default: False
|
||||
effects:
|
||||
- pulse:
|
||||
name: "Breathing"
|
||||
min_brightness: 0.15
|
||||
max_brightness: 0.45
|
||||
transition_length: 3000ms
|
||||
update_interval: 3000ms
|
||||
- addressable_scan:
|
||||
name: "Scanner"
|
||||
scan_width: 2
|
||||
move_interval: 100ms
|
||||
- flicker:
|
||||
name: "Alert Flash"
|
||||
alpha: 95%
|
||||
intensity: 1.5%
|
||||
- addressable_rainbow:
|
||||
name: "Rainbow Effect"
|
||||
speed: 10
|
||||
width: 50
|
||||
|
||||
i2c:
|
||||
sda: GPIO8
|
||||
scl: GPIO9
|
||||
scan: true
|
||||
id: bus_a
|
||||
frequency: 100kHz
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
name: "OLED Power"
|
||||
id: oled_power
|
||||
optimistic: true
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
turn_on_action:
|
||||
- logger.log: "OLED turned ON"
|
||||
turn_off_action:
|
||||
- logger.log: "OLED turned OFF"
|
||||
|
||||
select:
|
||||
- platform: template
|
||||
name: "LED Mode"
|
||||
id: led_mode
|
||||
options:
|
||||
- "Auto"
|
||||
- "Manual"
|
||||
- "Breathing"
|
||||
- "Scanner"
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
on_value:
|
||||
then:
|
||||
- script.execute: update_led_state
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
name: "IAQ LED Brightness"
|
||||
id: iaq_led_brightness
|
||||
min_value: 0.15
|
||||
max_value: 1.0
|
||||
step: 0.01
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
|
||||
sensor:
|
||||
- platform: uptime
|
||||
name: Uptime Sensor
|
||||
|
||||
- platform: homeassistant
|
||||
id: inside_temperature
|
||||
entity_id: $internal_temp_sensor
|
||||
internal: true
|
||||
|
||||
- platform: homeassistant
|
||||
id: outside_temperature
|
||||
entity_id: $outside_temp_sensor
|
||||
internal: true
|
||||
|
||||
- platform: homeassistant
|
||||
id: todays_forecast_high
|
||||
entity_id: $todays_forecast_high_entity
|
||||
internal: true
|
||||
|
||||
- platform: homeassistant
|
||||
id: todays_forecast_low
|
||||
entity_id: $todays_forecast_low_entity
|
||||
internal: true
|
||||
|
||||
- platform: pmsx003
|
||||
type: PMSX003
|
||||
pm_1_0:
|
||||
id: pm1
|
||||
name: "PM <1.0µm Concentration"
|
||||
pm_2_5:
|
||||
id: pm25
|
||||
name: "PM <2.5µm Concentration"
|
||||
pm_10_0:
|
||||
id: pm10
|
||||
name: "PM <10.0µm Concentration"
|
||||
|
||||
- platform: bme680
|
||||
temperature:
|
||||
name: "BME680 Temperature"
|
||||
id: bmetemp
|
||||
oversampling: 16x
|
||||
pressure:
|
||||
name: "BME680 Pressure"
|
||||
id: bmepressure
|
||||
humidity:
|
||||
name: "BME680 Humidity"
|
||||
id: bmehum
|
||||
gas_resistance:
|
||||
name: "BME680 Gas Resistance"
|
||||
id: bmegas
|
||||
address: 0x77
|
||||
update_interval: 10s
|
||||
|
||||
## CO²/VOC Sensor
|
||||
- platform: ccs811
|
||||
eco2:
|
||||
name: "CCS811 CO²"
|
||||
accuracy_decimals: 0
|
||||
id: eco2
|
||||
tvoc:
|
||||
name: "CCS811 T-VOC"
|
||||
accuracy_decimals: 0
|
||||
id: tvoc
|
||||
address: 0x5A
|
||||
update_interval: 10s
|
||||
temperature: bmetemp
|
||||
humidity: bmehum
|
||||
## After Calibration, Uncomment and change "baseline:"
|
||||
baseline: 0x9CB1
|
||||
|
||||
- platform: template
|
||||
name: "Humidity Sensor"
|
||||
id: humi
|
||||
unit_of_measurement: "%"
|
||||
accuracy_decimals: 1
|
||||
lambda: |-
|
||||
return id(bmehum).state;
|
||||
update_interval: 10s
|
||||
|
||||
- platform: wifi_signal
|
||||
name: AQ WiFi Signal
|
||||
update_interval: 60s
|
||||
|
||||
- platform: veml7700
|
||||
address: 0x10
|
||||
update_interval: 10s
|
||||
# short variant of sensor definition:
|
||||
ambient_light:
|
||||
name: "Ambient Light"
|
||||
id: ambient_light
|
||||
filters:
|
||||
- sliding_window_moving_average:
|
||||
window_size: 5
|
||||
send_every: 1
|
||||
|
||||
font:
|
||||
- file: "fonts/Roboto-Regular.ttf"
|
||||
id: robotto
|
||||
size: 10
|
||||
|
||||
- file: "fonts/Roboto-Regular.ttf"
|
||||
id: font2
|
||||
size: 12
|
||||
|
||||
- file: "fonts/Poppins-Regular.ttf"
|
||||
id: font1
|
||||
size: 10
|
||||
|
||||
- file: "fonts/Poppins-Regular.ttf"
|
||||
id: poppinslarger
|
||||
size: 12
|
||||
|
||||
- file: "fonts/Poppins-SemiBold.ttf"
|
||||
id: poppinsbold
|
||||
size: 10
|
||||
|
||||
- file: 'fonts/materialdesignicons-webfont.ttf'
|
||||
id: font3
|
||||
size: 18
|
||||
glyphs:
|
||||
- "\U000F13D5" #mdi:home-minus-outline
|
||||
|
||||
- file: 'fonts/materialdesignicons-webfont.ttf'
|
||||
id: font4
|
||||
size: 40
|
||||
glyphs:
|
||||
- "\U000F0594" #"clear-night"
|
||||
- "\U000F0590" #"cloudy"
|
||||
- "\U000F0591" #"fog"
|
||||
- "\U000F0592" #"hail"
|
||||
- "\U000F0593" #"lightning"
|
||||
- "\U000F067E" #"lightning-rainy"
|
||||
- "\U000F0595" #"partlycloudy"
|
||||
- "\U000F0596" #"pouring"
|
||||
- "\U000F0597" #"rainy"
|
||||
- "\U000F0598" #"snowy"
|
||||
- "\U000F067F" #"snowy-rainy"
|
||||
- "\U000F0599" #"sunny"
|
||||
- "\U000F059D" #"windy"
|
||||
- "\U000F059E" #"windy-variant"
|
||||
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
model: "SSD1306 128x64"
|
||||
address: 0x3C
|
||||
id: oled_display
|
||||
rotation: 0
|
||||
i2c_id: bus_a
|
||||
pages:
|
||||
- id: page_air_quality
|
||||
lambda: |-
|
||||
if (id(oled_power).state) { // only draw if display is ON
|
||||
{
|
||||
// Combine the final string first
|
||||
char full_text[64];
|
||||
|
||||
if (id(iaq_reading).has_state()) {
|
||||
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
|
||||
} else {
|
||||
sprintf(full_text, "Air Quality: Booting...");
|
||||
}
|
||||
|
||||
int x, y, w, h;
|
||||
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
|
||||
int center_x = (128 - w) / 2; // OLED is 128px wide
|
||||
|
||||
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
|
||||
}
|
||||
|
||||
// --- Temp + Humidity on same line ---
|
||||
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
|
||||
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
|
||||
|
||||
// --- Other sensor lines ---
|
||||
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(eco2).state);
|
||||
it.printf(0, 36, id(font1), "TVOC: %.0f ppb", id(tvoc).state);
|
||||
|
||||
if (!isnan(id(pm25).state)) {
|
||||
it.printf(0, 46, id(font1), "PM2.5: %.0f", id(pm25).state);
|
||||
} else {
|
||||
it.printf(0, 46, id(font1), "PM2.5: ---");
|
||||
}
|
||||
}
|
||||
|
||||
- id: page_environment
|
||||
lambda: |-
|
||||
if (id(oled_power).state) { // only draw if display is ON
|
||||
{
|
||||
// Combine the final string first
|
||||
char full_text[64];
|
||||
|
||||
if (id(iaq_reading).has_state()) {
|
||||
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
|
||||
} else {
|
||||
sprintf(full_text, "Air Quality: Booting...");
|
||||
}
|
||||
|
||||
int x, y, w, h;
|
||||
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
|
||||
int center_x = (128 - w) / 2; // OLED is 128px wide
|
||||
|
||||
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
|
||||
}
|
||||
// --- Temp + Humidity on same line ---
|
||||
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
|
||||
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
|
||||
// --- Other sensor lines ---
|
||||
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(ambient_light).state);
|
||||
}
|
||||
|
||||
- id: page_weather
|
||||
lambda: |-
|
||||
if (id(weather_state).has_state()) {
|
||||
std::map<std::string, std::string> weather_icon_map
|
||||
{
|
||||
{"clear-night", "\U000F0594"},
|
||||
{"cloudy", "\U000F0590"},
|
||||
{"fog", "\U000F0591"},
|
||||
{"hail", "\U000F0592"},
|
||||
{"lightning", "\U000F0593"},
|
||||
{"lightning-rainy", "\U000F067E"},
|
||||
{"partlycloudy", "\U000F0595"},
|
||||
{"pouring", "\U000F0596"},
|
||||
{"rainy", "\U000F0597"},
|
||||
{"snowy", "\U000F0598"},
|
||||
{"snowy-rainy", "\U000F067F"},
|
||||
{"sunny", "\U000F0599"},
|
||||
{"windy", "\U000F059D"},
|
||||
{"windy-variant", "\U000F059E"},
|
||||
};
|
||||
it.printf(0, it.get_height(), id(font4), TextAlign::BASELINE_LEFT, weather_icon_map[id(weather_state).state.c_str()].c_str());
|
||||
}
|
||||
|
||||
// Print time in HH:MM format
|
||||
it.strftime(0, 0, id(font1), TextAlign::TOP_LEFT, "%H:%M %a", id(homeassistant_time).now());
|
||||
|
||||
//Print day of week
|
||||
//it.strftime(40, 0, id(font1), TextAlign::TOP_LEFT, "%a", id(homeassistant_time).now());
|
||||
|
||||
it.line(0, 20, it.get_width(), 20);
|
||||
|
||||
//Print home icon
|
||||
//it.printf(70, 0, id(font3), "\U000F13D5");
|
||||
|
||||
// Print inside temperature (from homeassistant sensor)
|
||||
if (id(inside_temperature).has_state()) {
|
||||
it.printf(it.get_width(), 0, id(font1), TextAlign::TOP_RIGHT , "%7.1f°", id(inside_temperature).state);
|
||||
}
|
||||
|
||||
// Print outside temperature (from homeassistant sensor)
|
||||
if (id(outside_temperature).has_state()) {
|
||||
it.printf(42, 32, id(font2), "%.1f°", id(outside_temperature).state);
|
||||
}
|
||||
|
||||
// Print Forecast High
|
||||
if (id(todays_forecast_high).has_state()) {
|
||||
it.printf(it.get_width(), 32, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_high).state);
|
||||
}
|
||||
|
||||
// Print Forecast Low
|
||||
if (id(todays_forecast_low).has_state()) {
|
||||
it.printf(it.get_width(), 48, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_low).state);
|
||||
}
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: weather_state
|
||||
name: "Current Weather Icon"
|
||||
entity_id: $weather_entity
|
||||
internal: true
|
||||
|
||||
- platform: template
|
||||
name: "PM 2.5 Air Quality"
|
||||
icon: mdi:air-filter
|
||||
id: aq_reading
|
||||
lambda: |-
|
||||
if (id(pm25).state <= 12) {
|
||||
return {"Good"};
|
||||
}
|
||||
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
|
||||
return {"Moderate"};
|
||||
}
|
||||
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
|
||||
return {"Unhealthy(SG)"};
|
||||
}
|
||||
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
|
||||
return {"Unhealthy"};
|
||||
}
|
||||
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
|
||||
return {"Very Unhealthy"};
|
||||
}
|
||||
else if (id(pm25).state >= 250.5) {
|
||||
return {"Hazardous"};
|
||||
}
|
||||
return {};
|
||||
update_interval: 30s
|
||||
|
||||
- platform: template
|
||||
name: "PM 10 Air Quality"
|
||||
icon: mdi:air-filter
|
||||
id: aq_10_reading
|
||||
lambda: |-
|
||||
if (id(pm10).state <= 54) {
|
||||
return {"Good"};
|
||||
}
|
||||
else if ((id(pm10).state >= 55) && (id(pm10).state <= 154)) {
|
||||
return {"Moderate"};
|
||||
}
|
||||
else if ((id(pm10).state >= 155) && (id(pm10).state <= 254)) {
|
||||
return {"Unhealthy(SG)"};
|
||||
}
|
||||
else if ((id(pm10).state >= 255) && (id(pm10).state <= 354)) {
|
||||
return {"Unhealthy"};
|
||||
}
|
||||
else if ((id(pm10).state >= 355) && (id(pm10).state <= 424)) {
|
||||
return {"Very Unhealthy"};
|
||||
}
|
||||
else if (id(pm10).state >= 425) {
|
||||
return {"Hazardous"};
|
||||
}
|
||||
return {};
|
||||
update_interval: 30s
|
||||
|
||||
- platform: template
|
||||
name: "Livingroom IAQ"
|
||||
icon: "mdi:air-filter"
|
||||
id: iaq_reading
|
||||
lambda: |-
|
||||
// 1. Safety Check: Ensure all sensors have valid numbers
|
||||
if (isnan(id(humi).state) || isnan(id(pm25).state) || isnan(id(eco2).state) || isnan(id(tvoc).state)) {
|
||||
return {"Booting..."};
|
||||
}
|
||||
id(iaq_index) = 0;
|
||||
|
||||
if (id(humi).state < 10 or id(humi).state > 90) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
else if (id(humi).state < 20 or id(humi).state > 80) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(humi).state < 30 or id(humi).state > 70) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(humi).state < 40 or id(humi).state > 60) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(humi).state >= 40 and id(humi).state <= 60) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
|
||||
if (id(pm25).state <= 12) {
|
||||
id(iaq_index) += 6;
|
||||
}
|
||||
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(pm25).state >= 250.5) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(eco2).state <= 600) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(eco2).state <= 800) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(eco2).state <= 1500) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(eco2).state <= 1800) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(eco2).state > 1800) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(tvoc).state <= 65) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(tvoc).state <= 220) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(tvoc).state <= 660) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(tvoc).state <= 2200) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(tvoc).state > 2200) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
|
||||
|
||||
if (id(iaq_index) <= 11) {
|
||||
return {"Unhealthy"};
|
||||
}
|
||||
else if (id(iaq_index) <= 14) {
|
||||
return {"Poor"};
|
||||
}
|
||||
else if (id(iaq_index) <= 17) {
|
||||
return {"Moderate"};
|
||||
}
|
||||
else if (id(iaq_index) <= 19) {
|
||||
return {"Good"};
|
||||
}
|
||||
else if (id(iaq_index) > 19) {
|
||||
return {"Excellent"};
|
||||
}
|
||||
|
||||
return {};
|
||||
update_interval: 10s
|
||||
on_value:
|
||||
then:
|
||||
- script.execute: update_led_color
|
||||
|
||||
- platform: template
|
||||
name: "Livingroom IAQ Calculation"
|
||||
icon: "mdi:air-filter"
|
||||
id: iaq_reading_calculation
|
||||
lambda: |-
|
||||
id(iaq_index) = 0;
|
||||
|
||||
if (id(humi).state < 10 or id(humi).state > 90) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
else if (id(humi).state < 20 or id(humi).state > 80) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(humi).state < 30 or id(humi).state > 70) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(humi).state < 40 or id(humi).state > 60) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(humi).state >= 40 and id(humi).state <= 60) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
|
||||
if (id(pm25).state <= 12) {
|
||||
id(iaq_index) += 6;
|
||||
}
|
||||
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(pm25).state >= 250.5) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(eco2).state <= 600) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(eco2).state <= 800) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(eco2).state <= 1500) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(eco2).state <= 1800) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(eco2).state > 1800) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
if (id(tvoc).state <= 65) {
|
||||
id(iaq_index) += 5;
|
||||
}
|
||||
else if (id(tvoc).state <= 220) {
|
||||
id(iaq_index) += 4;
|
||||
}
|
||||
else if (id(tvoc).state <= 660) {
|
||||
id(iaq_index) += 3;
|
||||
}
|
||||
else if (id(tvoc).state <= 2200) {
|
||||
id(iaq_index) += 2;
|
||||
}
|
||||
else if (id(tvoc).state > 2200) {
|
||||
id(iaq_index) += 1;
|
||||
}
|
||||
|
||||
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
|
||||
return std::to_string(id(iaq_index));
|
||||
|
||||
update_interval: 10s
|
||||
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
|
||||
1388
esphome/chore-tracker-esphome.yaml
Normal file
1388
esphome/chore-tracker-esphome.yaml
Normal file
File diff suppressed because it is too large
Load Diff
115
esphome/chore-tracker/chore-tracker-dashboard.yaml
Normal file
115
esphome/chore-tracker/chore-tracker-dashboard.yaml
Normal file
@@ -0,0 +1,115 @@
|
||||
################################################################################
|
||||
# chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
||||
# Kids: Jordyn, Declan, Chloe
|
||||
################################################################################
|
||||
|
||||
title: "Chore Tracker"
|
||||
views:
|
||||
- title: Today
|
||||
icon: mdi:checkbox-marked-circle
|
||||
path: chores
|
||||
cards:
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: markdown
|
||||
content: >
|
||||
### Jordyn
|
||||
{{
|
||||
'All done!' if states('sensor.jordyn_all_chores_done') == 'True'
|
||||
else states('sensor.jordyn_chores_done_today') ~ '/4 chores done'
|
||||
}}
|
||||
|
||||
- type: markdown
|
||||
content: >
|
||||
### Declan
|
||||
{{
|
||||
'All done!' if states('sensor.declan_all_chores_done') == 'True'
|
||||
else states('sensor.declan_chores_done_today') ~ '/2 chores done'
|
||||
}}
|
||||
|
||||
- type: markdown
|
||||
content: >
|
||||
### Chloe
|
||||
{{
|
||||
'All done!' if states('sensor.chloe_all_chores_done') == 'True'
|
||||
else states('sensor.chloe_chores_done_today') ~ '/3 chores done'
|
||||
}}
|
||||
|
||||
- type: button
|
||||
name: "Reset ALL Chores"
|
||||
icon: mdi:restart-alert
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: input_button.press
|
||||
target:
|
||||
entity_id: input_button.reset_all_chores
|
||||
- type: entities
|
||||
title: " Jordyn's Chores"
|
||||
entities:
|
||||
- entity: switch.chore_tracker_jordyn_put_on_underwear
|
||||
name: " Put on underwear"
|
||||
|
||||
- entity: switch.chore_tracker_jordyn_brush_teeth
|
||||
name: " Brush Teeth"
|
||||
|
||||
- entity: switch.chore_tracker_jordyn_fill_water_bowls
|
||||
name: " Fill water bowls"
|
||||
|
||||
- entity: switch.chore_tracker_jordyn_restock_cat_food_cans
|
||||
name: " Restock Cat Food Cans"
|
||||
- type: divider
|
||||
- entity: sensor.jordyn_chores_done_today
|
||||
name: "Chores done today"
|
||||
- entity: sensor.jordyn_all_chores_done
|
||||
name: "All Done?"
|
||||
- type: button
|
||||
name: "Reset Jordyn's Chores"
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: input_button.press
|
||||
target:
|
||||
entity_id: input_button.jordyn_reset_chores
|
||||
- type: entities
|
||||
title: " Declan's Chores"
|
||||
entities:
|
||||
- entity: switch.chore_tracker_declan_take_morning_pill
|
||||
name: " Take morning pill"
|
||||
|
||||
- entity: switch.chore_tracker_declan_scoop_dog_poop
|
||||
name: " Scoop Dog Poop"
|
||||
- type: divider
|
||||
- entity: sensor.declan_chores_done_today
|
||||
name: "Chores done today"
|
||||
- entity: sensor.declan_all_chores_done
|
||||
name: "All Done?"
|
||||
- type: button
|
||||
name: "Reset Declan's Chores"
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: input_button.press
|
||||
target:
|
||||
entity_id: input_button.declan_reset_chores
|
||||
- type: entities
|
||||
title: " Chloe's Chores"
|
||||
entities:
|
||||
- entity: switch.chore_tracker_chloe_fill_kitty_feeders
|
||||
name: " Fill kitty feeders"
|
||||
|
||||
- entity: switch.chore_tracker_chloe_scoop_kitty_litter
|
||||
name: " Scoop Kitty Litter"
|
||||
|
||||
- entity: switch.chore_tracker_chloe_replace_kitty_litter_bags
|
||||
name: " Replace Kitty Litter Bags"
|
||||
- type: divider
|
||||
- entity: sensor.chloe_chores_done_today
|
||||
name: "Chores done today"
|
||||
- entity: sensor.chloe_all_chores_done
|
||||
name: "All Done?"
|
||||
- type: button
|
||||
name: "Reset Chloe's Chores"
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: input_button.press
|
||||
target:
|
||||
entity_id: input_button.chloe_reset_chores
|
||||
|
||||
76
esphome/chore-tracker/chores_config.yaml
Normal file
76
esphome/chore-tracker/chores_config.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
################################################################################
|
||||
# chores_config.yaml — YOUR SINGLE SOURCE OF TRUTH
|
||||
#
|
||||
# Edit this file to change kids, chores, and settings.
|
||||
# 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:
|
||||
device_name: chore-tracker
|
||||
friendly_name: "Chore Tracker"
|
||||
wifi_ssid: !secret wifi_ssid
|
||||
wifi_password: !secret wifi_password
|
||||
api_key: !secret api_encryption_key
|
||||
ota_password: !secret ota_password
|
||||
reset_time: "00:00:00"
|
||||
reminder_time: "18:00"
|
||||
notify_service: notify.notify
|
||||
|
||||
# ── KIDS ──────────────────────────────────────────────────────────────────────
|
||||
# avatar: MDI codepoint shown in the sidebar on the kid's chore page
|
||||
# color / color_dark: hex colours for that kid's sidebar
|
||||
#
|
||||
kids:
|
||||
- name: Jordyn
|
||||
avatar: "\U000F011B" # mdi:cat
|
||||
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: "2F3061" # #2F3061 is a darker version of #5F5980, and is easier to read when used as a background colour for the sidebar.
|
||||
chores:
|
||||
- name: Put on underwear
|
||||
icon: "\U000F02E3" # mdi:bed
|
||||
- name: Brush Teeth
|
||||
icon: "\U000F09A9" # mdi:toothbrush
|
||||
- name: Fill water bowls
|
||||
icon: "\U000F01AB" # mdi:cup-water
|
||||
- name: Restock Cat Food Cans
|
||||
icon: "\U000F171C" # mdi:food-turkey
|
||||
|
||||
- name: Declan
|
||||
avatar: "\U000F1477" # mdi:wizard-hat
|
||||
color: "720026" # #720026 is a deep red, used for the kid's sidebar background.
|
||||
color_dark: "4F000B" # #4F000B is a darker version of #720026, easier to read when used as a background colour for the sidebar.
|
||||
chores:
|
||||
- name: Take morning pill
|
||||
icon: "\U000F0402" # mdi:pill
|
||||
- name: Scoop Dog Poop
|
||||
icon: "\U000F01F7" # mdi:emoticon-poop
|
||||
|
||||
- name: Chloe
|
||||
avatar: "\U000F16A3" # mdi:robot-excited
|
||||
color: "73683B" # #73683B is a light brown, used for the kid's sidebar background.
|
||||
color_dark: "583E23" # #583E23 is a darker version of #73683B, easier to read when used as a background colour for the sidebar.
|
||||
chores:
|
||||
- name: Fill kitty feeders
|
||||
icon: "\U000F02A9" # mdi:bowl-outline
|
||||
- name: Scoop Kitty Litter
|
||||
icon: "\U000F01F7" # mdi:emoticon-poop
|
||||
- name: Replace Kitty Litter Bags
|
||||
icon: "\U000F01F7" # mdi:emoticon-poop
|
||||
949
esphome/chore-tracker/generate.py
Normal file
949
esphome/chore-tracker/generate.py
Normal file
@@ -0,0 +1,949 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
generate.py — Chore Tracker Code Generator
|
||||
==========================================
|
||||
Reads chores_config.yaml and generates:
|
||||
• chore-tracker-esphome.yaml (flash to ESP32 via ESPHome)
|
||||
• chore-tracker-ha.yaml (Home Assistant config)
|
||||
• chore-tracker-dashboard.yaml (Lovelace dashboard)
|
||||
|
||||
Target hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480)
|
||||
|
||||
Home screen behaviour:
|
||||
- Each kid's button has a RED outline while any chores remain undone
|
||||
- Button turns SOLID GREEN when ALL chores are complete
|
||||
- Resets to red automatically at midnight
|
||||
|
||||
Each kid must define their own chores list in chores_config.yaml.
|
||||
|
||||
Usage:
|
||||
python3 generate.py
|
||||
python3 generate.py --config my_other_config.yaml
|
||||
python3 generate.py --out ./output_dir
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("PyYAML not found. Install it with: pip install pyyaml")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Screen padding — space kept clear around all edges (pixels)
|
||||
PAD = 12
|
||||
|
||||
|
||||
def slug(name: str) -> str:
|
||||
return name.lower().replace(" ", "_").replace("-", "_")
|
||||
|
||||
def kid_slug(kid: dict) -> str:
|
||||
return slug(kid["name"])
|
||||
|
||||
def eid(kid: dict, chore: dict) -> str:
|
||||
return f"{kid_slug(kid)}_{slug(chore['name'])}"
|
||||
|
||||
def ha_switch(kid: dict, chore: dict) -> str:
|
||||
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:
|
||||
chores = kid["chores"]
|
||||
for c in chores:
|
||||
if "icon" not in c:
|
||||
c["icon"] = DEFAULT_ICON
|
||||
return chores
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ESPHome generator
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_esphome(cfg: dict) -> str:
|
||||
s = cfg["settings"]
|
||||
kids = cfg["kids"]
|
||||
|
||||
# ── Switches (one per chore per kid) ──────────────────────────────────────
|
||||
switch_blocks = []
|
||||
for kid in kids:
|
||||
chores = get_chores(kid)
|
||||
ks = kid_slug(kid)
|
||||
switch_blocks.append(f"\n # ── {kid['name']}'s chores ──────────────────────────\n")
|
||||
for chore in chores:
|
||||
e = eid(kid, chore)
|
||||
switch_blocks.append(f"""\
|
||||
- platform: template
|
||||
name: "{kid['name']} - {chore['name']}"
|
||||
id: {e}
|
||||
icon: mdi:checkbox-marked-circle
|
||||
optimistic: true
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
on_turn_on:
|
||||
then:
|
||||
- script.execute: update_{ks}_ui
|
||||
- homeassistant.service:
|
||||
service: switch.turn_on
|
||||
data:
|
||||
entity_id: switch.chore_tracker_{e}
|
||||
on_turn_off:
|
||||
then:
|
||||
- script.execute: update_{ks}_ui
|
||||
- homeassistant.service:
|
||||
service: switch.turn_off
|
||||
data:
|
||||
entity_id: switch.chore_tracker_{e}
|
||||
""")
|
||||
|
||||
# ── Sensors (chores-done count only — no stars) ────────────────────────
|
||||
sensor_blocks = []
|
||||
for kid in kids:
|
||||
chores = get_chores(kid)
|
||||
ks = kid_slug(kid)
|
||||
done_lines = "\n".join(
|
||||
f" if (id({eid(kid, c)}).state) done++;" for c in chores
|
||||
)
|
||||
sensor_blocks.append(f"""\
|
||||
- platform: template
|
||||
name: "{kid['name']} Chores Done"
|
||||
id: {ks}_chores_done
|
||||
accuracy_decimals: 0
|
||||
update_interval: 2s
|
||||
lambda: |-
|
||||
int done = 0;
|
||||
{done_lines}
|
||||
return done;
|
||||
|
||||
""")
|
||||
|
||||
# ── Midnight reset calls ──────────────────────────────────────────────────
|
||||
reset_calls = "\n".join(
|
||||
f" id(reset_{kid_slug(k)}_chores).execute();"
|
||||
for k in kids
|
||||
)
|
||||
|
||||
lvgl_pages = _gen_lvgl_pages(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"""\
|
||||
################################################################################
|
||||
# chore-tracker-esphome.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
||||
# Kids: {", ".join(k["name"] for k in kids)}
|
||||
#
|
||||
# HOME SCREEN BEHAVIOUR:
|
||||
# Red outline = chores incomplete
|
||||
# Solid green = all chores done
|
||||
# Resets to red automatically at midnight
|
||||
#
|
||||
# Hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480)
|
||||
################################################################################
|
||||
|
||||
esphome:
|
||||
name: {s["device_name"]}
|
||||
friendly_name: "{s["friendly_name"]}"
|
||||
on_boot:
|
||||
priority: -10
|
||||
then:
|
||||
- light.turn_on: backlight
|
||||
- lvgl.page.show: page_home
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
speed: 80MHz
|
||||
|
||||
wifi:
|
||||
ssid: {s["wifi_ssid"]}
|
||||
password: {s["wifi_password"]}
|
||||
ap:
|
||||
ssid: "{s['friendly_name']} Hotspot"
|
||||
password: "choretracker"
|
||||
|
||||
captive_portal:
|
||||
logger:
|
||||
level: INFO
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: {s["api_key"]}
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: {s["ota_password"]}
|
||||
|
||||
# ── I2C ───────────────────────────────────────────────────────────────────────
|
||||
i2c:
|
||||
sda: 8
|
||||
scl: 9
|
||||
|
||||
# ── 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:
|
||||
- platform: template
|
||||
id: lcd_backlight_out
|
||||
type: binary
|
||||
write_action:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return state;
|
||||
then:
|
||||
- switch.turn_on: lcd_backlight_raw
|
||||
else:
|
||||
- switch.turn_off: lcd_backlight_raw
|
||||
|
||||
light:
|
||||
- platform: binary
|
||||
name: "Display Backlight"
|
||||
output: lcd_backlight_out
|
||||
id: backlight
|
||||
|
||||
# ── Midnight reset ────────────────────────────────────────────────────────────
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
on_time:
|
||||
- hours: 0
|
||||
minutes: 0
|
||||
seconds: 0
|
||||
then:
|
||||
- lambda: |-
|
||||
{reset_calls}
|
||||
- lvgl.page.show: page_home
|
||||
|
||||
# ── Fonts (place files in /config/esphome/fonts/) ────────────────────────────
|
||||
font:
|
||||
- file: "fonts/Nunito-Black.ttf"
|
||||
id: font_title
|
||||
size: 40
|
||||
- file: "fonts/Nunito-ExtraBold.ttf"
|
||||
id: font_name
|
||||
size: 30
|
||||
- file: "fonts/Nunito-Bold.ttf"
|
||||
id: font_med
|
||||
size: 22
|
||||
- file: "fonts/Nunito-SemiBold.ttf"
|
||||
id: font_small
|
||||
size: 17
|
||||
- file: "fonts/Nunito-SemiBold.ttf"
|
||||
id: font_tiny
|
||||
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 — backlight raw + one per chore per kid ─────────────────────────
|
||||
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)}
|
||||
|
||||
# ── Sensors — reported to HA ─────────────────────────────────────────────────
|
||||
sensor:
|
||||
{"".join(sensor_blocks)}
|
||||
|
||||
# ── Scripts — reset + UI update ───────────────────────────────────────────────
|
||||
script:
|
||||
{scripts}
|
||||
|
||||
# ── LVGL UI ───────────────────────────────────────────────────────────────────
|
||||
lvgl:
|
||||
displays:
|
||||
- main_display
|
||||
touchscreens:
|
||||
- touch
|
||||
theme:
|
||||
button:
|
||||
radius: 20
|
||||
border_width: 0
|
||||
pages:
|
||||
{lvgl_pages}
|
||||
"""
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# LVGL page builder
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _gen_lvgl_pages(kids: list) -> str:
|
||||
pages = []
|
||||
|
||||
# ── HOME PAGE ─────────────────────────────────────────────────────────────
|
||||
n = len(kids)
|
||||
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)
|
||||
start_x = PAD + (usable_w - total_w) // 2
|
||||
btn_y = PAD + 118
|
||||
btn_h = 210
|
||||
|
||||
reset_all_calls = "\n".join(
|
||||
f" - script.execute: reset_{kid_slug(k)}_chores"
|
||||
for k in kids
|
||||
)
|
||||
|
||||
home_btns = ""
|
||||
for i, kid in enumerate(kids):
|
||||
x = start_x + i * (btn_w + 20)
|
||||
ks = kid_slug(kid)
|
||||
home_btns += f"""\
|
||||
- button:
|
||||
id: home_btn_{ks}
|
||||
x: {x}
|
||||
y: {btn_y}
|
||||
width: {btn_w}
|
||||
height: {btn_h}
|
||||
bg_color: 0xFF4757
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
radius: 24
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.page.show: page_{ks}
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
y: -45
|
||||
text: "{kid['avatar']}"
|
||||
text_font: font_mdi_large
|
||||
- label:
|
||||
align: CENTER
|
||||
y: 22
|
||||
text: "{kid['name']}"
|
||||
text_font: font_name
|
||||
text_color: 0xFFFFFF
|
||||
- label:
|
||||
id: home_status_{ks}
|
||||
align: CENTER
|
||||
y: 66
|
||||
text: "not done"
|
||||
text_font: font_tiny
|
||||
text_color: 0xFFFFFF
|
||||
|
||||
"""
|
||||
|
||||
pages.append(f"""\
|
||||
- id: page_home
|
||||
bg_color: 0xFFF9F0
|
||||
widgets:
|
||||
- label:
|
||||
x: 0
|
||||
y: 36
|
||||
width: {800 - PAD * 2}
|
||||
align: TOP_MID
|
||||
text: "Chore Tracker"
|
||||
text_font: font_title
|
||||
text_color: 0x2D3436
|
||||
- label:
|
||||
x: 0
|
||||
y: 90
|
||||
width: {800 - PAD * 2}
|
||||
align: TOP_MID
|
||||
text: "Tap a name to check off chores"
|
||||
text_font: font_small
|
||||
text_color: 0xB2BEC3
|
||||
- button:
|
||||
x: 640
|
||||
y: 428
|
||||
width: 148
|
||||
height: 40
|
||||
bg_color: 0xFF4757
|
||||
radius: 12
|
||||
on_click:
|
||||
then:
|
||||
{reset_all_calls}
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: "Reset All"
|
||||
text_color: 0xFFFFFF
|
||||
text_font: font_tiny
|
||||
{home_btns}""")
|
||||
|
||||
# ── PER-KID CHORE PAGES ───────────────────────────────────────────────────
|
||||
for kid in kids:
|
||||
chores = get_chores(kid)
|
||||
ks = kid_slug(kid)
|
||||
n_chores = len(chores)
|
||||
|
||||
cols = 3
|
||||
rows = (n_chores + cols - 1) // cols
|
||||
sidebar_w = 172
|
||||
gap = 10
|
||||
content_w = 800 - sidebar_w - gap - 12 # right pad
|
||||
card_w = (content_w - gap * (cols - 1)) // cols
|
||||
card_h = min(138, (480 - PAD - gap * (rows + 1)) // rows)
|
||||
|
||||
card_widgets = ""
|
||||
for idx, chore in enumerate(chores):
|
||||
row = idx // cols
|
||||
col = idx % cols
|
||||
x = sidebar_w + gap + col * (card_w + gap)
|
||||
y = 12 + row * (card_h + gap)
|
||||
e = eid(kid, chore)
|
||||
icon_y = -(card_h // 4)
|
||||
label_y = card_h // 8
|
||||
card_widgets += f"""\
|
||||
- button:
|
||||
id: card_{e}
|
||||
x: {x}
|
||||
y: {y}
|
||||
width: {card_w}
|
||||
height: {card_h}
|
||||
bg_color: 0xFFFFFF
|
||||
border_color: 0xEEEEEE
|
||||
border_width: 2
|
||||
radius: 20
|
||||
shadow_color: 0xCCCCCC
|
||||
shadow_width: 4
|
||||
shadow_ofs_y: 3
|
||||
on_click:
|
||||
then:
|
||||
- switch.toggle: {e}
|
||||
widgets:
|
||||
- label:
|
||||
id: check_{e}
|
||||
align: TOP_RIGHT
|
||||
x: -10
|
||||
y: 8
|
||||
text: ""
|
||||
text_font: font_med
|
||||
- label:
|
||||
align: CENTER
|
||||
y: {icon_y}
|
||||
text: "{chore['icon']}"
|
||||
text_font: font_mdi_small
|
||||
text_color: 0x{kid['color']}
|
||||
- label:
|
||||
align: CENTER
|
||||
y: {label_y}
|
||||
text: "{chore['name']}"
|
||||
text_font: font_small
|
||||
text_color: 0x2D3436
|
||||
|
||||
"""
|
||||
|
||||
pages.append(f"""\
|
||||
- id: page_{ks}
|
||||
bg_color: 0xFFF9F0
|
||||
widgets:
|
||||
|
||||
# ── Sidebar ──────────────────────────────────────────────────────────
|
||||
- obj:
|
||||
x: 0
|
||||
y: 0
|
||||
width: {sidebar_w}
|
||||
height: 480
|
||||
bg_color: 0x{kid['color']}
|
||||
border_width: 0
|
||||
radius: 0
|
||||
pad_all: 0
|
||||
widgets:
|
||||
- label:
|
||||
x: 0
|
||||
y: 22
|
||||
width: {sidebar_w}
|
||||
align: TOP_MID
|
||||
text: "{kid['avatar']}"
|
||||
text_font: font_mdi_large
|
||||
- label:
|
||||
x: 0
|
||||
y: 76
|
||||
width: {sidebar_w}
|
||||
align: TOP_MID
|
||||
text: "{kid['name']}"
|
||||
text_font: font_name
|
||||
text_color: 0xFFFFFF
|
||||
- bar:
|
||||
id: progress_bar_{ks}
|
||||
x: 14
|
||||
y: 132
|
||||
width: {sidebar_w - 28}
|
||||
height: 16
|
||||
bg_color: 0x{kid['color_dark']}
|
||||
indicator:
|
||||
bg_color: 0xFFFFFF
|
||||
value: 0
|
||||
min_value: 0
|
||||
max_value: {n_chores}
|
||||
- label:
|
||||
id: progress_label_{ks}
|
||||
x: 0
|
||||
y: 156
|
||||
width: {sidebar_w}
|
||||
align: TOP_MID
|
||||
text: "0 / {n_chores}"
|
||||
text_font: font_small
|
||||
text_color: 0xDDEEFF
|
||||
- label:
|
||||
id: all_done_label_{ks}
|
||||
x: 0
|
||||
y: 192
|
||||
width: {sidebar_w}
|
||||
align: TOP_MID
|
||||
text: ""
|
||||
text_font: font_small
|
||||
text_color: 0xFFFFFF
|
||||
- button:
|
||||
x: 14
|
||||
y: {480 - PAD - 44 - 6 - 44}
|
||||
width: {sidebar_w - 28}
|
||||
height: 44
|
||||
bg_color: 0xFF4757
|
||||
radius: 14
|
||||
on_click:
|
||||
then:
|
||||
- script.execute: reset_{ks}_chores
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: "Reset"
|
||||
text_color: 0xFFFFFF
|
||||
text_font: font_small
|
||||
- button:
|
||||
x: 14
|
||||
y: {480 - PAD - 44}
|
||||
width: {sidebar_w - 28}
|
||||
height: 44
|
||||
bg_color: 0x{kid['color_dark']}
|
||||
radius: 14
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.page.show: page_home
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: "Home"
|
||||
text_color: 0xFFFFFF
|
||||
text_font: font_small
|
||||
|
||||
# ── Chore cards ───────────────────────────────────────────────────────
|
||||
{card_widgets}""")
|
||||
|
||||
return "\n".join(pages)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Script builder
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _gen_scripts(kids: list) -> str:
|
||||
blocks = []
|
||||
for kid in kids:
|
||||
chores = get_chores(kid)
|
||||
ks = kid_slug(kid)
|
||||
n = len(chores)
|
||||
|
||||
card_updates = ""
|
||||
for chore in chores:
|
||||
e = eid(kid, chore)
|
||||
card_updates += f"""\
|
||||
if (id({e}).state) {{
|
||||
lv_obj_set_style_bg_color(id(card_{e}), lv_color_hex(0xF0FFF4), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_color(id(card_{e}), lv_color_hex(0x6BCB77), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(id(card_{e}), 3, LV_PART_MAIN);
|
||||
lv_label_set_text(id(check_{e}), "\\u2705");
|
||||
}} else {{
|
||||
lv_obj_set_style_bg_color(id(card_{e}), lv_color_hex(0xFFFFFF), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_color(id(card_{e}), lv_color_hex(0xEEEEEE), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(id(card_{e}), 2, LV_PART_MAIN);
|
||||
lv_label_set_text(id(check_{e}), "");
|
||||
}}
|
||||
"""
|
||||
|
||||
count_done = " + ".join(f"(id({eid(kid,c)}).state ? 1 : 0)" for c in chores)
|
||||
reset_lines = "\n".join(
|
||||
f" id({eid(kid,c)}).turn_off();" for c in chores
|
||||
)
|
||||
|
||||
blocks.append(f"""\
|
||||
- id: reset_{ks}_chores
|
||||
mode: single
|
||||
then:
|
||||
- lambda: |-
|
||||
{reset_lines}
|
||||
- script.execute: update_{ks}_ui
|
||||
|
||||
""")
|
||||
|
||||
blocks.append(f"""\
|
||||
- id: update_{ks}_ui
|
||||
mode: single
|
||||
then:
|
||||
- lambda: |-
|
||||
int done = {count_done};
|
||||
int total = {n};
|
||||
char buf[24];
|
||||
|
||||
// Progress bar + label
|
||||
lv_bar_set_value(id(progress_bar_{ks}), done, LV_ANIM_ON);
|
||||
snprintf(buf, sizeof(buf), "%d / {n}", done);
|
||||
lv_label_set_text(id(progress_label_{ks}), buf);
|
||||
|
||||
// All-done message in sidebar
|
||||
lv_label_set_text(id(all_done_label_{ks}), done == total ? "All done!" : "");
|
||||
|
||||
// Home button: RED outline = incomplete, SOLID GREEN = all done
|
||||
if (done == total) {{
|
||||
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_border_width(id(home_btn_{ks}), 0, LV_PART_MAIN);
|
||||
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);
|
||||
}} else {{
|
||||
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(0xFF4757), 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);
|
||||
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);
|
||||
}}
|
||||
|
||||
// Card colours
|
||||
{card_updates}
|
||||
""")
|
||||
|
||||
return "\n".join(blocks)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Home Assistant YAML generator
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_ha(cfg: dict) -> str:
|
||||
s = cfg["settings"]
|
||||
kids = cfg["kids"]
|
||||
|
||||
sensor_blocks = []
|
||||
for kid in kids:
|
||||
chores = get_chores(kid)
|
||||
ks = kid_slug(kid)
|
||||
n = len(chores)
|
||||
states_list = ", ".join(f"states('{ha_switch(kid,c)}')" for c in chores)
|
||||
|
||||
sensor_blocks.append(f"""\
|
||||
- name: "{kid['name']} Chores Done Today"
|
||||
unique_id: {ks}_chores_done_today
|
||||
icon: mdi:check-circle
|
||||
state: >
|
||||
{{% set chores = [{states_list}] %}}
|
||||
{{{{ chores | select('equalto', 'on') | list | count }}}}
|
||||
|
||||
- name: "{kid['name']} All Chores Done"
|
||||
unique_id: {ks}_all_chores_done
|
||||
icon: mdi:check-all
|
||||
state: >
|
||||
{{{{ states('sensor.{ks}_chores_done_today') | int == {n} }}}}
|
||||
|
||||
""")
|
||||
|
||||
input_buttons = "".join(f"""\
|
||||
{kid_slug(k)}_reset_chores:
|
||||
name: "Reset {k['name']}'s Chores"
|
||||
icon: mdi:restart
|
||||
""" for k in kids)
|
||||
input_buttons += """\
|
||||
reset_all_chores:
|
||||
name: "Reset All Chores"
|
||||
icon: mdi:restart-alert
|
||||
"""
|
||||
|
||||
auto_blocks = []
|
||||
|
||||
for kid in kids:
|
||||
ks = kid_slug(kid)
|
||||
off_calls = "\n".join(
|
||||
f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}"
|
||||
for c in get_chores(kid)
|
||||
)
|
||||
auto_blocks.append(f"""\
|
||||
- id: {ks}_reset_from_ha
|
||||
alias: "Chore Tracker - Reset {kid['name']}'s chores from HA"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: input_button.{ks}_reset_chores
|
||||
action:
|
||||
{off_calls}
|
||||
|
||||
""")
|
||||
|
||||
all_off = "\n".join(
|
||||
f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}"
|
||||
for kid in kids for c in get_chores(kid)
|
||||
)
|
||||
auto_blocks.append(f"""\
|
||||
- id: reset_all_from_ha
|
||||
alias: "Chore Tracker - Reset ALL chores from HA"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: input_button.reset_all_chores
|
||||
action:
|
||||
{all_off}
|
||||
|
||||
""")
|
||||
|
||||
for kid in kids:
|
||||
ks = kid_slug(kid)
|
||||
n = len(get_chores(kid))
|
||||
auto_blocks.append(f"""\
|
||||
- id: {ks}_all_done_notify
|
||||
alias: "Chore Tracker - {kid['name']} all done!"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: sensor.{ks}_all_chores_done
|
||||
to: "True"
|
||||
action:
|
||||
- service: {s['notify_service']}
|
||||
data:
|
||||
title: "{kid['name']} finished all chores!"
|
||||
message: "{kid['name']} completed all {n} chores today!"
|
||||
|
||||
""")
|
||||
|
||||
reminder_actions = "".join(f"""\
|
||||
- if:
|
||||
condition: template
|
||||
value_template: "{{{{ states('sensor.{kid_slug(k)}_all_chores_done') != 'True' }}}}"
|
||||
then:
|
||||
- service: {s['notify_service']}
|
||||
data:
|
||||
title: "{k['name']} has unfinished chores"
|
||||
message: >
|
||||
{k['name']} has done
|
||||
{{{{ states('sensor.{kid_slug(k)}_chores_done_today') }}}}/{len(get_chores(k))} chores today.
|
||||
""" for k in kids)
|
||||
|
||||
auto_blocks.append(f"""\
|
||||
- id: chore_reminder_evening
|
||||
alias: "Chore Tracker - Evening reminder"
|
||||
trigger:
|
||||
- platform: time
|
||||
at: "{s['reminder_time']}:00"
|
||||
action:
|
||||
{reminder_actions}
|
||||
""")
|
||||
|
||||
return f"""\
|
||||
################################################################################
|
||||
# chore-tracker-ha.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
||||
# Kids: {", ".join(k["name"] for k in kids)}
|
||||
#
|
||||
# BIDIRECTIONAL SYNC:
|
||||
# Screen to HA: Each switch calls homeassistant.service on toggle
|
||||
# HA to Screen: ESPHome native API handles this automatically
|
||||
################################################################################
|
||||
|
||||
input_button:
|
||||
{input_buttons}
|
||||
template:
|
||||
- sensor:
|
||||
{"".join(sensor_blocks)}
|
||||
automation:
|
||||
{"".join(auto_blocks)}
|
||||
"""
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Lovelace dashboard generator
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_dashboard(cfg: dict) -> str:
|
||||
kids = cfg["kids"]
|
||||
|
||||
summary_cards = "\n".join(f"""\
|
||||
- type: markdown
|
||||
content: >
|
||||
### {kid['avatar']} {kid['name']}
|
||||
{{{{
|
||||
'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'
|
||||
}}}}
|
||||
""" for kid in kids)
|
||||
|
||||
chore_cards = []
|
||||
for kid in kids:
|
||||
chores = get_chores(kid)
|
||||
ks = kid_slug(kid)
|
||||
chore_rows = "\n".join(f"""\
|
||||
- entity: {ha_switch(kid, c)}
|
||||
name: "{c['icon']} {c['name']}"
|
||||
""" for c in chores)
|
||||
|
||||
chore_cards.append(f"""\
|
||||
- type: entities
|
||||
title: "{kid['avatar']} {kid['name']}'s Chores"
|
||||
entities:
|
||||
{chore_rows} - type: divider
|
||||
- entity: sensor.{ks}_chores_done_today
|
||||
name: "Chores done today"
|
||||
- entity: sensor.{ks}_all_chores_done
|
||||
name: "All Done?"
|
||||
- type: button
|
||||
name: "Reset {kid['name']}'s Chores"
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: input_button.press
|
||||
target:
|
||||
entity_id: input_button.{ks}_reset_chores
|
||||
""")
|
||||
|
||||
return f"""\
|
||||
################################################################################
|
||||
# chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
||||
# Kids: {", ".join(k["name"] for k in kids)}
|
||||
################################################################################
|
||||
|
||||
title: "Chore Tracker"
|
||||
views:
|
||||
- title: Today
|
||||
icon: mdi:checkbox-marked-circle
|
||||
path: chores
|
||||
cards:
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
{summary_cards}
|
||||
- type: button
|
||||
name: "Reset ALL Chores"
|
||||
icon: mdi:restart-alert
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: input_button.press
|
||||
target:
|
||||
entity_id: input_button.reset_all_chores
|
||||
{"".join(chore_cards)}
|
||||
"""
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Main
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Chore Tracker config generator")
|
||||
parser.add_argument("--config", default="chores_config.yaml")
|
||||
parser.add_argument("--out", default=".")
|
||||
args = parser.parse_args()
|
||||
|
||||
config_path = Path(args.config)
|
||||
if not config_path.exists():
|
||||
print(f"❌ Config not found: {config_path}")
|
||||
sys.exit(1)
|
||||
|
||||
out_dir = Path(args.out)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class SecretLoader(yaml.SafeLoader):
|
||||
pass
|
||||
SecretLoader.add_constructor(
|
||||
"!secret",
|
||||
lambda loader, node: f"!secret {loader.construct_scalar(node)}"
|
||||
)
|
||||
|
||||
with open(config_path) as f:
|
||||
cfg = yaml.load(f, Loader=SecretLoader)
|
||||
|
||||
kids = cfg.get("kids", [])
|
||||
if not kids:
|
||||
print("❌ No kids defined in config!")
|
||||
sys.exit(1)
|
||||
|
||||
errors = [k["name"] for k in kids if "chores" not in k or not k["chores"]]
|
||||
if errors:
|
||||
print(f"❌ These kids have no chores defined: {', '.join(errors)}")
|
||||
print(" Add a 'chores:' list under each kid in chores_config.yaml")
|
||||
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)
|
||||
print(f"✅ Config loaded: {len(kids)} kid(s), {total_chores} total chores")
|
||||
for k in kids:
|
||||
chores = get_chores(k)
|
||||
print(f" {k['avatar']} {k['name']}: {len(chores)} chores"
|
||||
f" — {', '.join(c['name'] for c in chores)}")
|
||||
print()
|
||||
|
||||
files = {
|
||||
"chore-tracker-esphome.yaml": gen_esphome(cfg),
|
||||
"chore-tracker-ha.yaml": gen_ha(cfg),
|
||||
"chore-tracker-dashboard.yaml": gen_dashboard(cfg),
|
||||
}
|
||||
|
||||
for name, content in files.items():
|
||||
path = out_dir / name
|
||||
path.write_text(content)
|
||||
print(f"📄 {path}")
|
||||
|
||||
print()
|
||||
print("✅ Done!")
|
||||
print(" 1. Flash chore-tracker-esphome.yaml -> ESPHome dashboard")
|
||||
print(" 2. Merge chore-tracker-ha.yaml -> HA config + restart HA")
|
||||
print(" 3. Paste chore-tracker-dashboard.yaml -> New HA dashboard")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
72
esphome/declan-a1-camera.yaml
Normal file
72
esphome/declan-a1-camera.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
substitutions:
|
||||
device_name: declan-a1-camera
|
||||
friendly_name: "Declan's A1 Camera"
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: ${friendly_name}
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
|
||||
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: "${friendly_name} Fallback"
|
||||
password: !secret fallback_password
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
captive_portal:
|
||||
|
||||
i2c:
|
||||
- id: camera_i2c
|
||||
sda: GPIO26
|
||||
scl: GPIO27
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
speed: 80MHz
|
||||
|
||||
esp32_camera:
|
||||
external_clock:
|
||||
pin: GPIO0
|
||||
frequency: 20MHz
|
||||
i2c_id: camera_i2c
|
||||
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
|
||||
vsync_pin: GPIO25
|
||||
href_pin: GPIO23
|
||||
pixel_clock_pin: GPIO22
|
||||
power_down_pin: GPIO32
|
||||
resolution: 1024x768
|
||||
jpeg_quality: 15
|
||||
vertical_flip: true
|
||||
horizontal_mirror: false
|
||||
max_framerate: 20 fps
|
||||
idle_framerate: 0.05 fps
|
||||
name: ${device_name}
|
||||
|
||||
esp32_camera_web_server:
|
||||
- port: 8080
|
||||
mode: STREAM
|
||||
- port: 8081
|
||||
mode: SNAPSHOT
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
name: "${device_name}-flash"
|
||||
pin: 4
|
||||
95
esphome/esp32-h2c-camera.yaml
Normal file
95
esphome/esp32-h2c-camera.yaml
Normal file
@@ -0,0 +1,95 @@
|
||||
substitutions:
|
||||
device_name: esp32-h2c-camera
|
||||
friendly_name: "3D Printer Camera"
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: ${friendly_name}
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Required for ESP32 camera buffering.
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
logger:
|
||||
|
||||
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: "${friendly_name} Fallback"
|
||||
password: !secret fallback_password
|
||||
|
||||
captive_portal:
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
i2c:
|
||||
- id: camera_i2c
|
||||
sda: GPIO8
|
||||
scl: GPIO9
|
||||
scan: false
|
||||
|
||||
esp32_camera:
|
||||
name: "${friendly_name}"
|
||||
external_clock:
|
||||
pin: GPIO5
|
||||
frequency: 20MHz
|
||||
i2c_id: camera_i2c
|
||||
data_pins: [GPIO16, GPIO18, GPIO21, GPIO17, GPIO14, GPIO7, GPIO6, GPIO4]
|
||||
vsync_pin: GPIO1
|
||||
href_pin: GPIO2
|
||||
pixel_clock_pin: GPIO15
|
||||
|
||||
# Start conservative for reliability; increase later if stable.
|
||||
resolution: 800X600
|
||||
jpeg_quality: 15
|
||||
max_framerate: 20 fps
|
||||
idle_framerate: 0.2 fps
|
||||
frame_buffer_count: 1
|
||||
|
||||
# Common orientation defaults for this module.
|
||||
vertical_flip: true
|
||||
horizontal_mirror: false
|
||||
brightness: 1
|
||||
|
||||
# Camera endpoints:
|
||||
# Stream: http://<ip>:8080
|
||||
# Snapshot: http://<ip>:8081
|
||||
esp32_camera_web_server:
|
||||
- port: 8080
|
||||
mode: stream
|
||||
- port: 8081
|
||||
mode: snapshot
|
||||
|
||||
switch:
|
||||
# DFRobot board IR illumination control pin.
|
||||
- platform: gpio
|
||||
name: "${friendly_name} IR LED"
|
||||
pin: GPIO47
|
||||
restore_mode: ALWAYS_OFF
|
||||
|
||||
sensor:
|
||||
- platform: wifi_signal
|
||||
name: "${friendly_name} WiFi Signal"
|
||||
update_interval: 60s
|
||||
|
||||
- platform: uptime
|
||||
name: "${friendly_name} Uptime"
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
name: "${friendly_name} Restart"
|
||||
91
esphome/esp32-saturn4-cam.yaml
Normal file
91
esphome/esp32-saturn4-cam.yaml
Normal file
@@ -0,0 +1,91 @@
|
||||
esphome:
|
||||
name: esp32-saturn4-cam
|
||||
friendly_name: esp32-saturn4-cam
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
substitutions:
|
||||
devicename: "esp32-saturn4-cam"
|
||||
hostname: "esp32-saturn4-cam"
|
||||
#friendly_name:
|
||||
device_description: ESP32-CAM module in Saturn 4 Ultra Resin printer.
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: "3Zxo91fAfA7wpZ4DAYFxGarCRIGQr+0rUJH2taJo7ds="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "d2e50b2ad36ed7de4ecbfd90725765e3"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "Esp32-Saturn4-Cam"
|
||||
password: "klRzTYuqPJXm"
|
||||
|
||||
captive_portal:
|
||||
|
||||
# Example configuration entry
|
||||
i2c:
|
||||
- id: camera_i2c
|
||||
sda: GPIO26
|
||||
scl: GPIO27
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
speed: 80MHz
|
||||
|
||||
# Flashlight # you can control this flashlight within Homeassistant or mqtt to shine on the birds. use it with an timer!
|
||||
output:
|
||||
- platform: ledc
|
||||
pin: GPIO4
|
||||
id: gpio_4
|
||||
channel: 2
|
||||
|
||||
## GPIO_4 is the flash light pin
|
||||
light:
|
||||
- platform: monochromatic
|
||||
output: gpio_4
|
||||
name: $hostname light
|
||||
restore_mode: RESTORE_AND_OFF
|
||||
icon: mdi:flash
|
||||
|
||||
esp32_camera:
|
||||
external_clock:
|
||||
pin: GPIO0
|
||||
frequency: 20MHz
|
||||
i2c_id: camera_i2c
|
||||
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
|
||||
vsync_pin: GPIO25
|
||||
href_pin: GPIO23
|
||||
pixel_clock_pin: GPIO22
|
||||
power_down_pin: GPIO32
|
||||
resolution: 640x480 # you can use: [1600x1200 , 1280x1024 , 1024x768 , 800x600, 640x480 ] for best performance (FPS) use 1024 of lower resolution.
|
||||
jpeg_quality: 12 #The JPEG quality that the camera should encode images with. From 10 (best) to 63 (worst)
|
||||
aec2: true
|
||||
ae_level: 2
|
||||
brightness: 2 #The brightness to apply to the picture, default 0
|
||||
contrast: 2
|
||||
saturation: -2
|
||||
vertical_flip: False
|
||||
|
||||
# Image settings
|
||||
name: $devicename
|
||||
# ...
|
||||
|
||||
esp32_camera_web_server:
|
||||
- port: 80
|
||||
mode: STREAM
|
||||
- port: 8080
|
||||
mode: SNAPSHOT
|
||||
107
esphome/esphome-web-0fdcf4.yaml
Normal file
107
esphome/esphome-web-0fdcf4.yaml
Normal file
@@ -0,0 +1,107 @@
|
||||
esphome:
|
||||
name: salt-sensor
|
||||
friendly_name: Salt-Sensor
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
framework:
|
||||
type: arduino
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: "EaifDkFuB9N+qDKHW6B4k/n495FWLVmcp6DBODKzK10="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "7ce613915c51252c2810c07c889b4a5e"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "Salt-Sensor Fallback Hotspot"
|
||||
password: "ZoBRDcj54rwW"
|
||||
|
||||
captive_portal:
|
||||
|
||||
# Enable Web server.
|
||||
web_server:
|
||||
port: 80
|
||||
# Sync time with Home Assistant.
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: homeassistant_time
|
||||
|
||||
# Text sensors with general information.
|
||||
text_sensor:
|
||||
# Expose ESPHome version as sensor.
|
||||
- platform: version
|
||||
name: salt_level_sensor ESPHome Version
|
||||
# Expose WiFi information as sensors.
|
||||
- platform: wifi_info
|
||||
ip_address:
|
||||
name: salt_level_sensor IP
|
||||
ssid:
|
||||
name: salt_level_sensor SSID
|
||||
bssid:
|
||||
name: salt_level_sensor BSSID
|
||||
sensor:
|
||||
# Uptime sensor.
|
||||
- platform: uptime
|
||||
name: salt_level_sensor Uptime
|
||||
|
||||
# WiFi Signal sensor.
|
||||
- platform: wifi_signal
|
||||
name: salt_level_sensor WiFi Signal
|
||||
update_interval: 60s
|
||||
|
||||
# Ultrasonic sensor to measure salt level.
|
||||
# - platform: ultrasonic
|
||||
# trigger_pin: GPIO6
|
||||
# echo_pin: GPIO7
|
||||
# name: "Salt level in percent"
|
||||
# update_interval: 1h
|
||||
# unit_of_measurement: "%"
|
||||
# # pulse_time: 20us
|
||||
# # accuracy_decimals: 4
|
||||
# filters:
|
||||
# - lambda: return(0.42-x)*(100/0.42);
|
||||
# # - multiply: 100
|
||||
# - exponential_moving_average:
|
||||
# alpha: 0.001
|
||||
# send_every: 1
|
||||
# - clamp:
|
||||
# min_value: 20
|
||||
# max_value: 100
|
||||
# on_value_range:
|
||||
# above: 200
|
||||
# then:
|
||||
# - homeassistant.service:
|
||||
# service: notify.mailgun
|
||||
# data:
|
||||
# target:
|
||||
# -"joshua@cnjmail.com"
|
||||
# title: "Add Salt"
|
||||
# message: "Add Salt to the Water Softener Tank"
|
||||
# - homeassistant.service:
|
||||
# service: notify.mobile_app_joshuas_iphone_of_pain
|
||||
# data:
|
||||
# message: Water softener is low on salt, please add salt.
|
||||
|
||||
# Ultrasonic sensor to measure salt level.
|
||||
- platform: ultrasonic
|
||||
trigger_pin: GPIO6
|
||||
echo_pin: GPIO7
|
||||
name: "Salt level in cm"
|
||||
update_interval: .5h
|
||||
unit_of_measurement: "cm"
|
||||
# pulse_time: 20us
|
||||
# accuracy_decimals: 4
|
||||
filters:
|
||||
- lambda: return(0.83-x)*(100/0.83);
|
||||
79
esphome/fdm-exhaust-fan-control.yaml
Normal file
79
esphome/fdm-exhaust-fan-control.yaml
Normal file
@@ -0,0 +1,79 @@
|
||||
substitutions:
|
||||
device_name: fdm-exhaust-fan-control
|
||||
friendly_name: "FDM Exhaust Fan Control"
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: ${friendly_name}
|
||||
|
||||
esp32:
|
||||
board: seeed_xiao_esp32c3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: !secret ota_password
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_iot_ssid
|
||||
password: !secret wifi_password
|
||||
ap:
|
||||
ssid: "${friendly_name} Fallback"
|
||||
password: !secret fallback_password
|
||||
|
||||
captive_portal:
|
||||
|
||||
web_server:
|
||||
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:
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO3 # D1 on XIAO
|
||||
inverted: true
|
||||
id: fan_80mm_output
|
||||
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO4 # D2 on XIAO
|
||||
inverted: true
|
||||
id: fan_120mm_output
|
||||
|
||||
# Fan entities for Home Assistant
|
||||
fan:
|
||||
- platform: binary
|
||||
name: "80mm Exhaust Fan"
|
||||
id: fan_80mm
|
||||
output: fan_80mm_output
|
||||
|
||||
- platform: binary
|
||||
name: "120mm Exhaust Fan"
|
||||
id: fan_120mm
|
||||
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;
|
||||
5133
esphome/ha-entities.txt
Normal file
5133
esphome/ha-entities.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
esphome/ha-remote/components/max17043/__init__.py
Normal file
1
esphome/ha-remote/components/max17043/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@blacknell"]
|
||||
20
esphome/ha-remote/components/max17043/automation.h
Normal file
20
esphome/ha-remote/components/max17043/automation.h
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
#pragma once
|
||||
#include "esphome/core/automation.h"
|
||||
#include "max17043.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace max17043 {
|
||||
|
||||
template<typename... Ts> class SleepAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {}
|
||||
|
||||
void play(Ts... x) override { this->max17043_->sleep_mode(); }
|
||||
|
||||
protected:
|
||||
MAX17043Component *max17043_;
|
||||
};
|
||||
|
||||
} // namespace max17043
|
||||
} // namespace esphome
|
||||
72
esphome/ha-remote/components/max17043/max17043.cpp
Normal file
72
esphome/ha-remote/components/max17043/max17043.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "max17043.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace max17043 {
|
||||
|
||||
// MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert
|
||||
// Consult the datasheet at https://www.analog.com/en/products/max17043.html
|
||||
|
||||
static const char *const TAG = "max17043";
|
||||
|
||||
static const uint8_t MAX17043_VCELL = 0x02;
|
||||
static const uint8_t MAX17043_SOC = 0x04;
|
||||
static const uint8_t MAX17043_CONFIG = 0x0c;
|
||||
|
||||
static const uint16_t MAX17043_CONFIG_POWER_UP_DEFAULT = 0x971C;
|
||||
static const uint16_t MAX17043_CONFIG_SAFE_MASK = 0xFF1F; // mask out sleep bit (7), unused bit (6) and alert bit (4)
|
||||
static const uint16_t MAX17043_CONFIG_SLEEP_MASK = 0x0080;
|
||||
|
||||
void MAX17043Component::update() {
|
||||
uint16_t raw_voltage, raw_percent;
|
||||
|
||||
if (this->voltage_sensor_ != nullptr) {
|
||||
if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) {
|
||||
this->status_set_warning("Unable to read MAX17043_VCELL");
|
||||
} else {
|
||||
float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0;
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
if (this->battery_remaining_sensor_ != nullptr) {
|
||||
if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) {
|
||||
this->status_set_warning("Unable to read MAX17043_SOC");
|
||||
} else {
|
||||
float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff));
|
||||
this->battery_remaining_sensor_->publish_state(percent);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MAX17043Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up MAX17043...");
|
||||
// Compatible mode for MAX17048/variant boards:
|
||||
// avoid setup-time register writes on the shared touch I2C bus.
|
||||
}
|
||||
|
||||
void MAX17043Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MAX17043:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with MAX17043 failed");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Battery Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_);
|
||||
}
|
||||
|
||||
float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void MAX17043Component::sleep_mode() {
|
||||
if (!this->is_failed()) {
|
||||
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) {
|
||||
ESP_LOGW(TAG, "Unable to write the sleep bit to config register");
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace max17043
|
||||
} // namespace esphome
|
||||
29
esphome/ha-remote/components/max17043/max17043.h
Normal file
29
esphome/ha-remote/components/max17043/max17043.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace max17043 {
|
||||
|
||||
class MAX17043Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
void sleep_mode();
|
||||
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) {
|
||||
battery_remaining_sensor_ = battery_remaining_sensor;
|
||||
}
|
||||
|
||||
protected:
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *battery_remaining_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace max17043
|
||||
} // namespace esphome
|
||||
77
esphome/ha-remote/components/max17043/sensor.py
Normal file
77
esphome/ha-remote/components/max17043/sensor.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
max17043_ns = cg.esphome_ns.namespace("max17043")
|
||||
MAX17043Component = max17043_ns.class_(
|
||||
"MAX17043Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
# Actions
|
||||
SleepAction = max17043_ns.class_("SleepAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MAX17043Component),
|
||||
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x36))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if voltage_config := config.get(CONF_BATTERY_VOLTAGE):
|
||||
sens = await sensor.new_sensor(voltage_config)
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
|
||||
if CONF_BATTERY_LEVEL in config:
|
||||
sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
|
||||
cg.add(var.set_battery_remaining_sensor(sens))
|
||||
|
||||
|
||||
MAX17043_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(MAX17043Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA)
|
||||
async def max17043_sleep_mode_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
176
esphome/ha-remote/ha-remote-1.base.yaml
Normal file
176
esphome/ha-remote/ha-remote-1.base.yaml
Normal file
@@ -0,0 +1,176 @@
|
||||
psram:
|
||||
mode: octal
|
||||
speed: 80MHz
|
||||
|
||||
i2c:
|
||||
id: i2c_main
|
||||
sda: 8
|
||||
scl: 9
|
||||
frequency: 400kHz
|
||||
scan: false
|
||||
|
||||
# CH422G I/O expander (Waveshare uses it for LCD reset/backlight/touch reset)
|
||||
ch422g:
|
||||
- id: ch422g_hub
|
||||
i2c_id: i2c_main
|
||||
|
||||
# --- Backlight control (CH422G IO2 is common for Waveshare LCD BL) ---
|
||||
switch:
|
||||
- platform: gpio
|
||||
name: "LCD Backlight Raw"
|
||||
id: lcd_backlight_raw
|
||||
restore_mode: ALWAYS_ON
|
||||
pin:
|
||||
ch422g: ch422g_hub
|
||||
number: 2
|
||||
mode:
|
||||
output: true
|
||||
|
||||
# A nicer HA-exposed control (so you can also automate it from HA)
|
||||
light:
|
||||
- platform: binary
|
||||
name: "HA Remote Backlight"
|
||||
output: lcd_backlight_out
|
||||
id: ha_remote_backlight
|
||||
|
||||
output:
|
||||
- platform: template
|
||||
id: lcd_backlight_out
|
||||
type: binary
|
||||
write_action:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return state;
|
||||
then:
|
||||
- switch.turn_on: lcd_backlight_raw
|
||||
else:
|
||||
- switch.turn_off: lcd_backlight_raw
|
||||
|
||||
# --- Inactivity tracking (dim/off + wake on touch) ---
|
||||
globals:
|
||||
- id: last_activity_ms
|
||||
type: uint32_t
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
interval:
|
||||
- interval: 1s
|
||||
then:
|
||||
- lambda: |-
|
||||
if (id(last_activity_ms) == 0) id(last_activity_ms) = millis();
|
||||
|
||||
# Turn off backlight ONCE at 2 minutes idle.
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
const uint32_t idle_s = (millis() - id(last_activity_ms)) / 1000;
|
||||
return idle_s == 120;
|
||||
then:
|
||||
- light.turn_off: ha_remote_backlight
|
||||
|
||||
# --- 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
|
||||
|
||||
# --- Touch ---
|
||||
touchscreen:
|
||||
platform: gt911
|
||||
id: touch_panel
|
||||
i2c_id: i2c_main
|
||||
interrupt_pin: 4
|
||||
reset_pin:
|
||||
ch422g: ch422g_hub
|
||||
number: 1
|
||||
mode:
|
||||
output: true
|
||||
on_touch:
|
||||
then:
|
||||
- lambda: |-
|
||||
id(last_activity_ms) = millis();
|
||||
- light.turn_on: ha_remote_backlight
|
||||
|
||||
# --- MDI Icon Font ---
|
||||
font:
|
||||
- file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf"
|
||||
id: mdi_icons
|
||||
size: 24
|
||||
bpp: 4
|
||||
glyphs:
|
||||
# Tile icons
|
||||
- "\U000F0335" # mdi:lightbulb
|
||||
- "\U000F0425" # mdi:power
|
||||
- "\U000F0426" # mdi:power-plug
|
||||
- "\U000F07E9" # mdi:power-socket-us
|
||||
- "\U000F1A26" # mdi:toggle-switch-variant-off
|
||||
# Battery status icons
|
||||
- "\U000F0079" # mdi:battery
|
||||
- "\U000F12A1" # mdi:battery-low
|
||||
- "\U000F12A2" # mdi:battery-medium
|
||||
- "\U000F12A3" # mdi:battery-high
|
||||
- "\U000F12A4" # mdi:battery-charging-low
|
||||
- "\U000F12A5" # mdi:battery-charging-medium
|
||||
- "\U000F12A6" # mdi:battery-charging-high
|
||||
- "\U000F0091" # mdi:battery-unknown
|
||||
- "\U000F10CD" # mdi:battery-alert-variant-outline
|
||||
# Navigation bar icons
|
||||
- "\U000F04B9" # mdi:sofa
|
||||
- "\U000F06B5" # mdi:lamp
|
||||
- "\U000F04DE" # mdi:stove
|
||||
- "\U000F12BD" # mdi:stairs-up
|
||||
- "\U000F1239" # mdi:desk
|
||||
- "\U000F06D9" # mdi:garage
|
||||
- "\U000F0531" # mdi:tree
|
||||
|
||||
# --- LVGL UI ---
|
||||
# Note: On ESP32-S3-Touch-LCD-7, GPIO14 is used by the RGB display bus,
|
||||
# so it cannot be reused as ADC for battery telemetry in this display mode.
|
||||
|
||||
# --- Battery fuel gauge (Adafruit MAX17048 via compatible driver) ---
|
||||
sensor:
|
||||
- platform: max17043
|
||||
id: max17048_battery
|
||||
i2c_id: i2c_main
|
||||
update_interval: 120s
|
||||
battery_voltage:
|
||||
name: "Remote Battery Voltage"
|
||||
id: remote_battery_voltage
|
||||
entity_category: diagnostic
|
||||
device_class: voltage
|
||||
state_class: measurement
|
||||
accuracy_decimals: 3
|
||||
on_value:
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("battery", "Voltage: %.3f V", x);
|
||||
battery_level:
|
||||
name: "Remote Battery Level"
|
||||
id: remote_battery_level
|
||||
entity_category: diagnostic
|
||||
device_class: battery
|
||||
state_class: measurement
|
||||
accuracy_decimals: 0
|
||||
on_value:
|
||||
then:
|
||||
- lambda: |-
|
||||
const float pct = x;
|
||||
ESP_LOGI("battery", "Level: %.0f%%", pct);
|
||||
# - platform: custom
|
||||
# lambda: |-
|
||||
# auto max17048_sensor = new MAX17048Sensor();
|
||||
# App.register_component(max17048_sensor);
|
||||
# return {max17048_sensor->voltage_sensor, max17048_sensor->percentage_sensor};
|
||||
# sensors:
|
||||
# - name: "Voltage"
|
||||
# unit_of_measurement: V
|
||||
# accuracy_decimals: 2
|
||||
# - name: "Percentage"
|
||||
# unit_of_measurement: '%'
|
||||
383
esphome/ha-remote/ha-remote-1.bindings.yaml
Normal file
383
esphome/ha-remote/ha-remote-1.bindings.yaml
Normal file
@@ -0,0 +1,383 @@
|
||||
text_sensor:
|
||||
- platform: wifi_info
|
||||
ip_address:
|
||||
name: "HA Remote IP"
|
||||
id: ip_addr
|
||||
- platform: homeassistant
|
||||
id: ts_family_room_tv_stand
|
||||
entity_id: light.family_room_tv_stand
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_family_room_tv_stand
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_family_room_tv_stand);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_small_family_room_lamp
|
||||
entity_id: light.small_family_room_lamp
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_small_family_room_lamp
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_small_family_room_lamp);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_living_room_lamp_1
|
||||
entity_id: light.living_room_lamp_1
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_family_room_standing_lamp
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x3D0002) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_family_room_standing_lamp);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_living_room_light_2
|
||||
entity_id: light.living_room_light_2
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_living_room_main_light
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_living_room_main_light);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_patio_light_1
|
||||
entity_id: light.patio_light_1
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_living_room_patio_light
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lvgl.widget.update:
|
||||
id: btn_outside_patio_light
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t off_color = lv_color_hex(0xE3E2E6);
|
||||
auto *btn1 = id(btn_living_room_patio_light);
|
||||
auto *btn2 = id(btn_outside_patio_light);
|
||||
if (x == "on") {
|
||||
lv_obj_set_style_text_color(btn1, lv_color_hex(0x1A0C00), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn1); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn1, i), lv_color_hex(0x1A0C00), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
lv_obj_set_style_text_color(btn2, lv_color_hex(0x001A33), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn2); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn2, i), lv_color_hex(0x001A33), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
} else {
|
||||
lv_obj_set_style_text_color(btn1, off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn1); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn1, i), off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
lv_obj_set_style_text_color(btn2, off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn2); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn2, i), off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_kitchen_sink_light
|
||||
entity_id: light.kitchen_sink_light
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_kitchen_sink_light
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_kitchen_sink_light);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_office_lamp_2
|
||||
entity_id: light.office_lamp_2
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_office_lamp
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_office_lamp);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_office_led_strip
|
||||
entity_id: light.led_strip_controller_led_strip_controller
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_office_led_strip
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_office_led_strip);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_garage_cam_light
|
||||
entity_id: light.esp32_saturn4_cam_esp32_saturn4_cam_light
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_garage_cam_light
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_garage_cam_light);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_office_echo_plug
|
||||
entity_id: switch.office_echo_plug
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_office_echo_plug
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_office_echo_plug);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_office_end_table_lamp_outlet
|
||||
entity_id: switch.office_end_table_lamp_socket_1
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_office_end_table_lamp_outlet
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_office_end_table_lamp_outlet);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_office_wax_warmer_outlet
|
||||
entity_id: switch.office_wax_warmer_socket_1
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_office_wax_warmer_outlet
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1E0F3F) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_office_wax_warmer_outlet);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_outside_lamppost_outlet_1
|
||||
entity_id: switch.lamppost_outlets_socket_1
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_outside_lamppost_outlet_1
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_outside_lamppost_outlet_1);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_outside_lamppost_outlet_2
|
||||
entity_id: switch.lamppost_outlets_socket_2
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_outside_lamppost_outlet_2
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_outside_lamppost_outlet_2);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_outside_porch_decor_outlet
|
||||
entity_id: switch.washing_machine_socket_1
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_outside_porch_decor_outlet
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_outside_porch_decor_outlet);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_living_room_garland_switch
|
||||
entity_id: switch.big_family_room_lamp
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_living_room_garland_switch
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_living_room_garland_switch);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_living_room_calendar_switch
|
||||
entity_id: switch.digital_calendar
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_living_room_calendar_switch
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1E0F3F) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_living_room_calendar_switch);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_upstairs_airquality_oled
|
||||
entity_id: switch.airqualitysensor_3_oled_power
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_upstairs_airquality_oled
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_upstairs_airquality_oled);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_upstairs_camera_motion
|
||||
entity_id: switch.upstairs_camera_motion_detection
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_upstairs_camera_motion
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_upstairs_camera_motion);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_garage_fume_exhaust_fan
|
||||
entity_id: switch.fume_exhaust_fan
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_garage_fume_exhaust_fan
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_garage_fume_exhaust_fan);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
- platform: homeassistant
|
||||
id: ts_garage_resin_printer_heater
|
||||
entity_id: switch.resin_printer_heater
|
||||
internal: true
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.widget.update:
|
||||
id: btn_garage_resin_printer_heater
|
||||
state:
|
||||
checked: !lambda return x == "on";
|
||||
- lambda: |-
|
||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6);
|
||||
auto *btn = id(btn_garage_resin_printer_heater);
|
||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
||||
}
|
||||
|
||||
2719
esphome/ha-remote/ha-remote-1.ui.yaml
Normal file
2719
esphome/ha-remote/ha-remote-1.ui.yaml
Normal file
File diff suppressed because it is too large
Load Diff
161
esphome/home-energy-monitor.yaml
Normal file
161
esphome/home-energy-monitor.yaml
Normal file
@@ -0,0 +1,161 @@
|
||||
substitutions:
|
||||
friendly_name: "Home Energy Monitor"
|
||||
device_name: home-energy-monitor
|
||||
update_time: 10s
|
||||
# SCT-013-000 (100A/50mA) calibration value
|
||||
current_cal: '27518'
|
||||
# Jameco 9VAC Transformer (board v1.3+)
|
||||
voltage_cal: '7305'
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: ${friendly_name}
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
# Enable OTA updates
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
# Optional: static IP for reliability
|
||||
# manual_ip:
|
||||
# static_ip: 192.168.1.50
|
||||
# gateway: 192.168.1.1
|
||||
# subnet: 255.255.255.0
|
||||
|
||||
# Fallback hotspot if Wi-Fi fails
|
||||
ap:
|
||||
ssid: ${device_name}
|
||||
password: !secret wifi_password
|
||||
|
||||
captive_portal:
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
# SPI bus configuration
|
||||
spi:
|
||||
clk_pin: 18
|
||||
miso_pin: 19
|
||||
mosi_pin: 23
|
||||
|
||||
sensor:
|
||||
# Wi-Fi signal strength
|
||||
- platform: wifi_signal
|
||||
name: ${device_name} WiFi Signal
|
||||
update_interval: 60s
|
||||
|
||||
# ──────────────────────────────────
|
||||
# IC1: Channels 1-3 (CS pin 5)
|
||||
# ──────────────────────────────────
|
||||
- platform: atm90e32
|
||||
cs_pin: 5
|
||||
phase_a:
|
||||
voltage:
|
||||
name: ${device_name} Volts
|
||||
id: ic1Volts
|
||||
accuracy_decimals: 1
|
||||
current:
|
||||
name: ${device_name} CT1 Amps
|
||||
id: ct1Amps
|
||||
power:
|
||||
name: ${device_name} CT1 Watts
|
||||
id: ct1Watts
|
||||
gain_voltage: ${voltage_cal}
|
||||
gain_ct: ${current_cal}
|
||||
phase_b:
|
||||
current:
|
||||
name: ${device_name} CT2 Amps
|
||||
id: ct2Amps
|
||||
power:
|
||||
name: ${device_name} CT2 Watts
|
||||
id: ct2Watts
|
||||
gain_voltage: ${voltage_cal}
|
||||
gain_ct: ${current_cal}
|
||||
phase_c:
|
||||
current:
|
||||
name: ${device_name} CT3 Amps
|
||||
id: ct3Amps
|
||||
power:
|
||||
name: ${device_name} CT3 Watts
|
||||
id: ct3Watts
|
||||
gain_voltage: ${voltage_cal}
|
||||
gain_ct: ${current_cal}
|
||||
frequency:
|
||||
name: ${device_name} Frequency
|
||||
line_frequency: 60Hz
|
||||
gain_pga: 1X
|
||||
update_interval: ${update_time}
|
||||
|
||||
# ──────────────────────────────────
|
||||
# IC2: Channels 4-6 (CS pin 4)
|
||||
# ──────────────────────────────────
|
||||
- platform: atm90e32
|
||||
cs_pin: 4
|
||||
phase_a:
|
||||
current:
|
||||
name: ${device_name} CT4 Amps
|
||||
id: ct4Amps
|
||||
power:
|
||||
name: ${device_name} CT4 Watts
|
||||
id: ct4Watts
|
||||
gain_voltage: ${voltage_cal}
|
||||
gain_ct: ${current_cal}
|
||||
phase_b:
|
||||
current:
|
||||
name: ${device_name} CT5 Amps
|
||||
id: ct5Amps
|
||||
power:
|
||||
name: ${device_name} CT5 Watts
|
||||
id: ct5Watts
|
||||
gain_voltage: ${voltage_cal}
|
||||
gain_ct: ${current_cal}
|
||||
phase_c:
|
||||
current:
|
||||
name: ${device_name} CT6 Amps
|
||||
id: ct6Amps
|
||||
power:
|
||||
name: ${device_name} CT6 Watts
|
||||
id: ct6Watts
|
||||
gain_voltage: ${voltage_cal}
|
||||
gain_ct: ${current_cal}
|
||||
line_frequency: 60Hz
|
||||
gain_pga: 1X
|
||||
update_interval: ${update_time}
|
||||
|
||||
# ──────────────────────────────────
|
||||
# Calculated: Total Home Power
|
||||
# (CT1 = Main Leg 1, CT2 = Main Leg 2)
|
||||
# ──────────────────────────────────
|
||||
- platform: template
|
||||
name: ${device_name} Total Watts
|
||||
id: totalWatts
|
||||
lambda: "return id(ct1Watts).state + id(ct2Watts).state;"
|
||||
accuracy_decimals: 1
|
||||
unit_of_measurement: W
|
||||
device_class: power
|
||||
update_interval: ${update_time}
|
||||
|
||||
- platform: total_daily_energy
|
||||
name: ${device_name} Total Daily Energy
|
||||
power_id: totalWatts
|
||||
unit_of_measurement: kWh
|
||||
accuracy_decimals: 2
|
||||
filters:
|
||||
- multiply: 0.001
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: homeassistant_time
|
||||
78
esphome/jordyn-a1-camera.yaml
Normal file
78
esphome/jordyn-a1-camera.yaml
Normal file
@@ -0,0 +1,78 @@
|
||||
esphome:
|
||||
name: jordyn-a1-camera
|
||||
friendly_name: jordyn-a1-camera
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
#cpu_frequency: 240MHZ
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: "PDB5LTKAcNWMC+MeE03/SKpqX42pcnITzmfwu761LjI="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "d207583702e4b979e28a43f6f52d19ae"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "Jordyn-A1-Camera"
|
||||
password: "jN4rzGokp5mt"
|
||||
|
||||
captive_portal:
|
||||
|
||||
i2c:
|
||||
- id: camera_i2c
|
||||
sda: GPIO26
|
||||
scl: GPIO27
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
speed: 80MHz
|
||||
|
||||
esp32_camera:
|
||||
external_clock:
|
||||
pin: GPIO0
|
||||
frequency: 20MHz
|
||||
i2c_id: camera_i2c
|
||||
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
|
||||
vsync_pin: GPIO25
|
||||
href_pin: GPIO23
|
||||
pixel_clock_pin: GPIO22
|
||||
power_down_pin: GPIO32
|
||||
resolution: 1024x768
|
||||
jpeg_quality: 20
|
||||
vertical_flip: False
|
||||
horizontal_mirror: False
|
||||
max_framerate: 40 fps
|
||||
idle_framerate: 0.05 fps
|
||||
|
||||
# Image settings
|
||||
name: esp32-jordyn-a1-cam
|
||||
# brightness: 1
|
||||
# #contrast: 0
|
||||
# agc_gain_ceiling: 2X
|
||||
# agc_mode: MANUAL
|
||||
# agc_value: 5
|
||||
# ...
|
||||
|
||||
esp32_camera_web_server:
|
||||
- port: 80
|
||||
mode: STREAM
|
||||
- port: 8080
|
||||
mode: SNAPSHOT
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
name: "jordyn-a1-cam-flash"
|
||||
pin: 4
|
||||
158
esphome/led-controller-declan-a1.yaml
Normal file
158
esphome/led-controller-declan-a1.yaml
Normal file
@@ -0,0 +1,158 @@
|
||||
substitutions:
|
||||
device_name: led-controller-declan-a1
|
||||
friendly_name: "Declan's LED Controller"
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: ${friendly_name}
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
# Enable over-the-air updates
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: !secret ota_password
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_iot_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "${friendly_name} Fallback"
|
||||
password: !secret fallback_password
|
||||
|
||||
captive_portal:
|
||||
|
||||
# Web server for standalone control (optional, but useful)
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
# LED Light configuration
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
rgb_order: GRB
|
||||
chipset: WS2812
|
||||
pin: GPIO7
|
||||
num_leds: 60
|
||||
name: "${friendly_name}"
|
||||
id: led_strip
|
||||
|
||||
# Default color on boot
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
|
||||
# Color correction for more accurate colors (optional)
|
||||
# Uncomment and adjust if your LEDs look too blue/green
|
||||
# color_correct: [100%, 100%, 100%]
|
||||
|
||||
# Effects - you can enable these for more features!
|
||||
effects:
|
||||
# Basic effects
|
||||
- random:
|
||||
name: "Random"
|
||||
transition_length: 5s
|
||||
update_interval: 7s
|
||||
|
||||
- strobe:
|
||||
name: "Strobe"
|
||||
colors:
|
||||
- state: true
|
||||
brightness: 100%
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 100%
|
||||
duration: 500ms
|
||||
- state: false
|
||||
duration: 250ms
|
||||
|
||||
- flicker:
|
||||
name: "Flicker"
|
||||
alpha: 95%
|
||||
intensity: 1.5%
|
||||
|
||||
# Rainbow effects
|
||||
- addressable_rainbow:
|
||||
name: "Rainbow"
|
||||
speed: 10
|
||||
width: 50
|
||||
|
||||
- addressable_color_wipe:
|
||||
name: "Color Wipe"
|
||||
colors:
|
||||
- red: 100%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
num_leds: 1
|
||||
- red: 0%
|
||||
green: 100%
|
||||
blue: 0%
|
||||
num_leds: 1
|
||||
- red: 0%
|
||||
green: 0%
|
||||
blue: 100%
|
||||
num_leds: 1
|
||||
add_led_interval: 100ms
|
||||
reverse: false
|
||||
|
||||
- addressable_scan:
|
||||
name: "Scan"
|
||||
move_interval: 100ms
|
||||
scan_width: 3
|
||||
|
||||
- addressable_twinkle:
|
||||
name: "Twinkle"
|
||||
twinkle_probability: 5%
|
||||
progress_interval: 4ms
|
||||
|
||||
- addressable_fireworks:
|
||||
name: "Fireworks"
|
||||
update_interval: 32ms
|
||||
spark_probability: 10%
|
||||
use_random_color: true
|
||||
fade_out_rate: 120
|
||||
|
||||
# Pulse effect
|
||||
- pulse:
|
||||
name: "Pulse"
|
||||
transition_length: 1s
|
||||
update_interval: 1s
|
||||
|
||||
# Sensors for monitoring
|
||||
sensor:
|
||||
# WiFi Signal Strength
|
||||
- platform: wifi_signal
|
||||
name: "${friendly_name} WiFi Signal"
|
||||
update_interval: 60s
|
||||
|
||||
# Uptime
|
||||
- platform: uptime
|
||||
name: "${friendly_name} Uptime"
|
||||
|
||||
# Text sensors
|
||||
text_sensor:
|
||||
# ESPHome version
|
||||
- platform: version
|
||||
name: "${friendly_name} ESPHome Version"
|
||||
|
||||
# WiFi info
|
||||
- platform: wifi_info
|
||||
ip_address:
|
||||
name: "${friendly_name} IP Address"
|
||||
ssid:
|
||||
name: "${friendly_name} Connected SSID"
|
||||
|
||||
# Button to restart ESP32
|
||||
button:
|
||||
- platform: restart
|
||||
name: "${friendly_name} Restart"
|
||||
158
esphome/led-controller-jordyn-a1.yaml
Normal file
158
esphome/led-controller-jordyn-a1.yaml
Normal file
@@ -0,0 +1,158 @@
|
||||
substitutions:
|
||||
device_name: led-controller-jordyn-a1
|
||||
friendly_name: "Jordyn's LED Controller"
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: ${friendly_name}
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
# Enable over-the-air updates
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: !secret ota_password
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_iot_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "${friendly_name} Fallback"
|
||||
password: !secret fallback_password
|
||||
|
||||
captive_portal:
|
||||
|
||||
# Web server for standalone control (optional, but useful)
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
# LED Light configuration
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
rgb_order: GRB
|
||||
chipset: WS2812
|
||||
pin: GPIO7
|
||||
num_leds: 60
|
||||
name: "${friendly_name}"
|
||||
id: led_strip
|
||||
|
||||
# Default color on boot
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
|
||||
# Color correction for more accurate colors (optional)
|
||||
# Uncomment and adjust if your LEDs look too blue/green
|
||||
# color_correct: [100%, 100%, 100%]
|
||||
|
||||
# Effects - you can enable these for more features!
|
||||
effects:
|
||||
# Basic effects
|
||||
- random:
|
||||
name: "Random"
|
||||
transition_length: 5s
|
||||
update_interval: 7s
|
||||
|
||||
- strobe:
|
||||
name: "Strobe"
|
||||
colors:
|
||||
- state: true
|
||||
brightness: 100%
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 100%
|
||||
duration: 500ms
|
||||
- state: false
|
||||
duration: 250ms
|
||||
|
||||
- flicker:
|
||||
name: "Flicker"
|
||||
alpha: 95%
|
||||
intensity: 1.5%
|
||||
|
||||
# Rainbow effects
|
||||
- addressable_rainbow:
|
||||
name: "Rainbow"
|
||||
speed: 10
|
||||
width: 50
|
||||
|
||||
- addressable_color_wipe:
|
||||
name: "Color Wipe"
|
||||
colors:
|
||||
- red: 100%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
num_leds: 1
|
||||
- red: 0%
|
||||
green: 100%
|
||||
blue: 0%
|
||||
num_leds: 1
|
||||
- red: 0%
|
||||
green: 0%
|
||||
blue: 100%
|
||||
num_leds: 1
|
||||
add_led_interval: 100ms
|
||||
reverse: false
|
||||
|
||||
- addressable_scan:
|
||||
name: "Scan"
|
||||
move_interval: 100ms
|
||||
scan_width: 3
|
||||
|
||||
- addressable_twinkle:
|
||||
name: "Twinkle"
|
||||
twinkle_probability: 5%
|
||||
progress_interval: 4ms
|
||||
|
||||
- addressable_fireworks:
|
||||
name: "Fireworks"
|
||||
update_interval: 32ms
|
||||
spark_probability: 10%
|
||||
use_random_color: true
|
||||
fade_out_rate: 120
|
||||
|
||||
# Pulse effect
|
||||
- pulse:
|
||||
name: "Pulse"
|
||||
transition_length: 1s
|
||||
update_interval: 1s
|
||||
|
||||
# Sensors for monitoring
|
||||
sensor:
|
||||
# WiFi Signal Strength
|
||||
- platform: wifi_signal
|
||||
name: "${friendly_name} WiFi Signal"
|
||||
update_interval: 60s
|
||||
|
||||
# Uptime
|
||||
- platform: uptime
|
||||
name: "${friendly_name} Uptime"
|
||||
|
||||
# Text sensors
|
||||
text_sensor:
|
||||
# ESPHome version
|
||||
- platform: version
|
||||
name: "${friendly_name} ESPHome Version"
|
||||
|
||||
# WiFi info
|
||||
- platform: wifi_info
|
||||
ip_address:
|
||||
name: "${friendly_name} IP Address"
|
||||
ssid:
|
||||
name: "${friendly_name} Connected SSID"
|
||||
|
||||
# Button to restart ESP32
|
||||
button:
|
||||
- platform: restart
|
||||
name: "${friendly_name} Restart"
|
||||
155
esphome/led-controller-polly-a1mini.yaml
Normal file
155
esphome/led-controller-polly-a1mini.yaml
Normal file
@@ -0,0 +1,155 @@
|
||||
substitutions:
|
||||
device_name: led-controller-polly-a1m
|
||||
friendly_name: "Polly's LED Controller"
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: ${friendly_name}
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
# Enable over-the-air updates
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: !secret ota_password
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_iot_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "${friendly_name} Fallback"
|
||||
password: !secret fallback_password
|
||||
|
||||
captive_portal:
|
||||
|
||||
# Web server for standalone control (optional, but useful)
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
# LED Light configuration
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
rgb_order: GRB
|
||||
chipset: WS2812
|
||||
pin: GPIO7
|
||||
num_leds: 10
|
||||
name: "${friendly_name}"
|
||||
id: led_strip
|
||||
|
||||
# Default color on boot
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
|
||||
|
||||
# Effects - you can enable these for more features!
|
||||
effects:
|
||||
# Basic effects
|
||||
- random:
|
||||
name: "Random"
|
||||
transition_length: 5s
|
||||
update_interval: 7s
|
||||
|
||||
- strobe:
|
||||
name: "Strobe"
|
||||
colors:
|
||||
- state: true
|
||||
brightness: 100%
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 100%
|
||||
duration: 500ms
|
||||
- state: false
|
||||
duration: 250ms
|
||||
|
||||
- flicker:
|
||||
name: "Flicker"
|
||||
alpha: 95%
|
||||
intensity: 1.5%
|
||||
|
||||
# Rainbow effects
|
||||
- addressable_rainbow:
|
||||
name: "Rainbow"
|
||||
speed: 10
|
||||
width: 50
|
||||
|
||||
- addressable_color_wipe:
|
||||
name: "Color Wipe"
|
||||
colors:
|
||||
- red: 100%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
num_leds: 1
|
||||
- red: 0%
|
||||
green: 100%
|
||||
blue: 0%
|
||||
num_leds: 1
|
||||
- red: 0%
|
||||
green: 0%
|
||||
blue: 100%
|
||||
num_leds: 1
|
||||
add_led_interval: 100ms
|
||||
reverse: false
|
||||
|
||||
- addressable_scan:
|
||||
name: "Scan"
|
||||
move_interval: 100ms
|
||||
scan_width: 3
|
||||
|
||||
- addressable_twinkle:
|
||||
name: "Twinkle"
|
||||
twinkle_probability: 5%
|
||||
progress_interval: 4ms
|
||||
|
||||
- addressable_fireworks:
|
||||
name: "Fireworks"
|
||||
update_interval: 32ms
|
||||
spark_probability: 10%
|
||||
use_random_color: true
|
||||
fade_out_rate: 120
|
||||
|
||||
# Pulse effect
|
||||
- pulse:
|
||||
name: "Pulse"
|
||||
transition_length: 1s
|
||||
update_interval: 1s
|
||||
|
||||
# Sensors for monitoring
|
||||
sensor:
|
||||
# WiFi Signal Strength
|
||||
- platform: wifi_signal
|
||||
name: "${friendly_name} WiFi Signal"
|
||||
update_interval: 60s
|
||||
|
||||
# Uptime
|
||||
- platform: uptime
|
||||
name: "${friendly_name} Uptime"
|
||||
|
||||
# Text sensors
|
||||
text_sensor:
|
||||
# ESPHome version
|
||||
- platform: version
|
||||
name: "${friendly_name} ESPHome Version"
|
||||
|
||||
# WiFi info
|
||||
- platform: wifi_info
|
||||
ip_address:
|
||||
name: "${friendly_name} IP Address"
|
||||
ssid:
|
||||
name: "${friendly_name} Connected SSID"
|
||||
|
||||
# Button to restart ESP32
|
||||
button:
|
||||
- platform: restart
|
||||
name: "${friendly_name} Restart"
|
||||
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
|
||||
88
esphome/polly-a1-mini-camera.yaml
Normal file
88
esphome/polly-a1-mini-camera.yaml
Normal file
@@ -0,0 +1,88 @@
|
||||
substitutions:
|
||||
device_name: polly-a1-mini-camera
|
||||
friendly_name: "Polly's A1 Camera"
|
||||
|
||||
esphome:
|
||||
name: ${device_name}
|
||||
friendly_name: ${friendly_name}
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
# minimum_chip_revision: 3.1
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
level: INFO
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: "iLF5QCgTVCwgaA5vjTGez03Dfjvns8JvkcwmUN4NXR8="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "58f86a5e60abe60886cec2f65a1d3da5"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_iot_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "${friendly_name} Fallback"
|
||||
password: !secret wifi_password
|
||||
|
||||
captive_portal:
|
||||
|
||||
# Web server for standalone control (optional, but useful)
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
i2c:
|
||||
- id: camera_i2c
|
||||
sda: GPIO26
|
||||
scl: GPIO27
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
speed: 80MHz
|
||||
|
||||
esp32_camera:
|
||||
external_clock:
|
||||
pin: GPIO0
|
||||
frequency: 20MHz
|
||||
i2c_id: camera_i2c
|
||||
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
|
||||
vsync_pin: GPIO25
|
||||
href_pin: GPIO23
|
||||
pixel_clock_pin: GPIO22
|
||||
power_down_pin: GPIO32
|
||||
resolution: 1024X768
|
||||
jpeg_quality: 20
|
||||
vertical_flip: False
|
||||
horizontal_mirror: True
|
||||
max_framerate: 20 fps
|
||||
idle_framerate: 0.05 fps
|
||||
|
||||
# Image settings
|
||||
name: ${device_name}
|
||||
# brightness: 1
|
||||
# contrast: 0
|
||||
# agc_gain_ceiling: 2X
|
||||
# agc_mode: MANUAL
|
||||
# agc_value: 5
|
||||
# ...
|
||||
|
||||
esp32_camera_web_server:
|
||||
- port: 8080
|
||||
mode: STREAM
|
||||
- port: 8081
|
||||
mode: SNAPSHOT
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
name: "${device_name}-flash"
|
||||
pin: 4
|
||||
|
||||
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"
|
||||
218
esphome/voice-assistant-1.yaml
Normal file
218
esphome/voice-assistant-1.yaml
Normal file
@@ -0,0 +1,218 @@
|
||||
esphome:
|
||||
name: voice-assistant-1
|
||||
friendly_name: Voice-Assistant-1
|
||||
name_add_mac_suffix: false
|
||||
platformio_options:
|
||||
board_build.flash_mode: dio
|
||||
build_flags:
|
||||
- -DCONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
||||
- -DCONFIG_FREERTOS_HZ=1000
|
||||
board_build.partitions: partitions.csv
|
||||
on_boot:
|
||||
priority: 600
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- micro_wake_word.start:
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: "oUOJJQHDaBvzKLAqfh3RwCEy9/KoFcj7AUgCSgTW/D8="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "48c1380a3d4d99d28bff36b2dc5c5e50"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_iot_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "Voice-Assistant-1"
|
||||
password: "pypEyxtF4xJ5"
|
||||
|
||||
captive_portal:
|
||||
|
||||
# web_server:
|
||||
# port: 80
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
speed: 80MHz
|
||||
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: led_ww
|
||||
rgb_order: GRB
|
||||
chipset: ws2812
|
||||
pin: GPIO16
|
||||
num_leds: 8
|
||||
name: "LED bar"
|
||||
effects:
|
||||
- pulse:
|
||||
- pulse:
|
||||
name: fast pulse
|
||||
transition_length: 250ms
|
||||
update_interval: 250ms
|
||||
- addressable_scan:
|
||||
name: scan
|
||||
move_interval: 100ms
|
||||
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:
|
||||
- platform: template
|
||||
id: mute
|
||||
name: "Mute microphone"
|
||||
optimistic: true
|
||||
on_turn_on:
|
||||
- micro_wake_word.stop:
|
||||
- voice_assistant.stop:
|
||||
- light.turn_on:
|
||||
id: led_ww
|
||||
red: 100%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
brightness: 30%
|
||||
- delay: 2s
|
||||
- light.turn_off:
|
||||
id: led_ww
|
||||
- light.turn_on:
|
||||
id: led_ww
|
||||
red: 100%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
brightness: 30%
|
||||
on_turn_off:
|
||||
- micro_wake_word.start:
|
||||
- light.turn_on:
|
||||
id: led_ww
|
||||
red: 0%
|
||||
green: 100%
|
||||
blue: 0%
|
||||
brightness: 60%
|
||||
effect: fast pulse
|
||||
- delay: 2s
|
||||
- light.turn_off:
|
||||
id: led_ww
|
||||
|
||||
i2s_audio:
|
||||
- id: i2s_mic
|
||||
i2s_lrclk_pin: GPIO6 # WS
|
||||
i2s_bclk_pin: GPIO7 # SCK
|
||||
- id: i2s_spk
|
||||
i2s_lrclk_pin: GPIO45 # Use free pins on your board
|
||||
i2s_bclk_pin: GPIO46 # Use free pins on your board
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: va_mic
|
||||
adc_type: external
|
||||
i2s_din_pin: GPIO4 # SD
|
||||
channel: left
|
||||
pdm: false
|
||||
bits_per_sample: 32bit
|
||||
i2s_audio_id: i2s_mic
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO8
|
||||
allow_other_uses: true
|
||||
id: set_low_speaker
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: va_speaker
|
||||
i2s_audio_id: i2s_spk
|
||||
dac_type: external
|
||||
i2s_dout_pin:
|
||||
number: GPIO8 # DIN pin of the MAX98357A Audio Amplifier
|
||||
allow_other_uses: true
|
||||
channel: mono
|
||||
bits_per_sample: 32bit
|
||||
sample_rate: 16000
|
||||
|
||||
micro_wake_word:
|
||||
models:
|
||||
- model: hey_jarvis
|
||||
on_wake_word_detected:
|
||||
- voice_assistant.start:
|
||||
wake_word: "Hey Jarvis"
|
||||
- light.turn_on:
|
||||
id: led_ww
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 100%
|
||||
brightness: 30%
|
||||
effect: scan
|
||||
|
||||
voice_assistant:
|
||||
id: va
|
||||
microphone: va_mic
|
||||
speaker: va_speaker
|
||||
use_wake_word: false
|
||||
noise_suppression_level: 2.0
|
||||
auto_gain: 31dBFS
|
||||
volume_multiplier: 8.0
|
||||
on_client_connected:
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- micro_wake_word.start:
|
||||
on_client_disconnected:
|
||||
- micro_wake_word.stop:
|
||||
- voice_assistant.stop:
|
||||
on_stt_end:
|
||||
then:
|
||||
- light.turn_off: led_ww
|
||||
on_error:
|
||||
- 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:
|
||||
then:
|
||||
- light.turn_off: led_ww
|
||||
- wait_until:
|
||||
not:
|
||||
voice_assistant.is_running:
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- micro_wake_word.start:
|
||||
222
packages/chore-tracker-ha.yaml
Normal file
222
packages/chore-tracker-ha.yaml
Normal file
@@ -0,0 +1,222 @@
|
||||
################################################################################
|
||||
# chore-tracker-ha.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
||||
# Kids: Jordyn, Declan, Chloe
|
||||
#
|
||||
# BIDIRECTIONAL SYNC:
|
||||
# Screen to HA: Each switch calls homeassistant.service on toggle
|
||||
# HA to Screen: ESPHome native API handles this automatically
|
||||
################################################################################
|
||||
|
||||
input_button:
|
||||
jordyn_reset_chores:
|
||||
name: "Reset Jordyn's Chores"
|
||||
icon: mdi:restart
|
||||
declan_reset_chores:
|
||||
name: "Reset Declan's Chores"
|
||||
icon: mdi:restart
|
||||
chloe_reset_chores:
|
||||
name: "Reset Chloe's Chores"
|
||||
icon: mdi:restart
|
||||
reset_all_chores:
|
||||
name: "Reset All Chores"
|
||||
icon: mdi:restart-alert
|
||||
|
||||
template:
|
||||
- sensor:
|
||||
- name: "Jordyn Chores Done Today"
|
||||
unique_id: jordyn_chores_done_today
|
||||
icon: mdi:check-circle
|
||||
state: >
|
||||
{% 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 }}
|
||||
|
||||
- name: "Jordyn All Chores Done"
|
||||
unique_id: jordyn_all_chores_done
|
||||
icon: mdi:check-all
|
||||
state: >
|
||||
{{ states('sensor.jordyn_chores_done_today') | int == 4 }}
|
||||
|
||||
- name: "Declan Chores Done Today"
|
||||
unique_id: declan_chores_done_today
|
||||
icon: mdi:check-circle
|
||||
state: >
|
||||
{% set chores = [states('switch.chore_tracker_declan_take_morning_pill'), states('switch.chore_tracker_declan_scoop_dog_poop')] %}
|
||||
{{ chores | select('equalto', 'on') | list | count }}
|
||||
|
||||
- name: "Declan All Chores Done"
|
||||
unique_id: declan_all_chores_done
|
||||
icon: mdi:check-all
|
||||
state: >
|
||||
{{ states('sensor.declan_chores_done_today') | int == 2 }}
|
||||
|
||||
- name: "Chloe Chores Done Today"
|
||||
unique_id: chloe_chores_done_today
|
||||
icon: mdi:check-circle
|
||||
state: >
|
||||
{% 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 }}
|
||||
|
||||
- name: "Chloe All Chores Done"
|
||||
unique_id: chloe_all_chores_done
|
||||
icon: mdi:check-all
|
||||
state: >
|
||||
{{ states('sensor.chloe_chores_done_today') | int == 3 }}
|
||||
|
||||
|
||||
automation:
|
||||
- id: jordyn_reset_from_ha
|
||||
alias: "Chore Tracker - Reset Jordyn's chores from HA"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: input_button.jordyn_reset_chores
|
||||
action:
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_jordyn_put_on_underwear
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_jordyn_brush_teeth
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_jordyn_fill_water_bowls
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_jordyn_restock_cat_food_cans
|
||||
|
||||
- id: declan_reset_from_ha
|
||||
alias: "Chore Tracker - Reset Declan's chores from HA"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: input_button.declan_reset_chores
|
||||
action:
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_declan_take_morning_pill
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_declan_scoop_dog_poop
|
||||
|
||||
- id: chloe_reset_from_ha
|
||||
alias: "Chore Tracker - Reset Chloe's chores from HA"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: input_button.chloe_reset_chores
|
||||
action:
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_chloe_fill_kitty_feeders
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_chloe_scoop_kitty_litter
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_chloe_replace_kitty_litter_bags
|
||||
|
||||
- id: reset_all_from_ha
|
||||
alias: "Chore Tracker - Reset ALL chores from HA"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: input_button.reset_all_chores
|
||||
action:
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_jordyn_put_on_underwear
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_jordyn_brush_teeth
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_jordyn_fill_water_bowls
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_jordyn_restock_cat_food_cans
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_declan_take_morning_pill
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_declan_scoop_dog_poop
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_chloe_fill_kitty_feeders
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_chloe_scoop_kitty_litter
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: switch.chore_tracker_chloe_replace_kitty_litter_bags
|
||||
|
||||
- id: jordyn_all_done_notify
|
||||
alias: "Chore Tracker - Jordyn all done!"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: sensor.jordyn_all_chores_done
|
||||
to: "True"
|
||||
action:
|
||||
- service: notify.notify
|
||||
data:
|
||||
title: "Jordyn finished all chores!"
|
||||
message: "Jordyn completed all 4 chores today!"
|
||||
|
||||
- id: declan_all_done_notify
|
||||
alias: "Chore Tracker - Declan all done!"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: sensor.declan_all_chores_done
|
||||
to: "True"
|
||||
action:
|
||||
- service: notify.notify
|
||||
data:
|
||||
title: "Declan finished all chores!"
|
||||
message: "Declan completed all 2 chores today!"
|
||||
|
||||
- id: chloe_all_done_notify
|
||||
alias: "Chore Tracker - Chloe all done!"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: sensor.chloe_all_chores_done
|
||||
to: "True"
|
||||
action:
|
||||
- service: notify.notify
|
||||
data:
|
||||
title: "Chloe finished all chores!"
|
||||
message: "Chloe completed all 3 chores today!"
|
||||
|
||||
- id: chore_reminder_evening
|
||||
alias: "Chore Tracker - Evening reminder"
|
||||
trigger:
|
||||
- platform: time
|
||||
at: "18:00:00"
|
||||
action:
|
||||
- if:
|
||||
condition: template
|
||||
value_template: "{{ states('sensor.jordyn_all_chores_done') != 'True' }}"
|
||||
then:
|
||||
- service: notify.notify
|
||||
data:
|
||||
title: "Jordyn has unfinished chores"
|
||||
message: >
|
||||
Jordyn has done
|
||||
{{ states('sensor.jordyn_chores_done_today') }}/4 chores today.
|
||||
- if:
|
||||
condition: template
|
||||
value_template: "{{ states('sensor.declan_all_chores_done') != 'True' }}"
|
||||
then:
|
||||
- service: notify.notify
|
||||
data:
|
||||
title: "Declan has unfinished chores"
|
||||
message: >
|
||||
Declan has done
|
||||
{{ states('sensor.declan_chores_done_today') }}/2 chores today.
|
||||
- if:
|
||||
condition: template
|
||||
value_template: "{{ states('sensor.chloe_all_chores_done') != 'True' }}"
|
||||
then:
|
||||
- service: notify.notify
|
||||
data:
|
||||
title: "Chloe has unfinished chores"
|
||||
message: >
|
||||
Chloe has done
|
||||
{{ states('sensor.chloe_chores_done_today') }}/3 chores today.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user