Compare commits
22 Commits
72ef3af179
...
battery-ic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3bf64281c | ||
|
|
ab4a22e20c | ||
|
|
19c088f81f | ||
|
|
9151032d80 | ||
|
|
049130b7fd | ||
|
|
7939382efc | ||
|
|
4a4cf43413 | ||
|
|
5ef090652a | ||
|
|
4c4102469b | ||
|
|
c83884e764 | ||
|
|
86bc59e7ff | ||
| 262450d48f | |||
| 0a0ec84845 | |||
|
|
557ccb9c5b | ||
|
|
584a37b6fb | ||
|
|
042e31686d | ||
|
|
3b38f166cb | ||
|
|
c9db0c6d0c | ||
|
|
2bf523f09b | ||
|
|
61a7acd366 | ||
|
|
c8565a9d49 | ||
| 9666eacfa1 |
@@ -1 +1 @@
|
|||||||
2025.6.3
|
2026.2.2
|
||||||
77
.gitignore
vendored
77
.gitignore
vendored
@@ -1,21 +1,64 @@
|
|||||||
# Database files
|
# Ignore everything by default
|
||||||
|
*
|
||||||
|
|
||||||
|
# Whitelist config files
|
||||||
|
!*.yaml
|
||||||
|
!*.yml
|
||||||
|
!.gitignore
|
||||||
|
!*.md
|
||||||
|
!*.sh
|
||||||
|
!*.js*
|
||||||
|
!*.txt*
|
||||||
|
!*.json*
|
||||||
|
!*.ui.yaml
|
||||||
|
|
||||||
|
# Whitelist subdirectory yaml files
|
||||||
|
!*/*.yaml
|
||||||
|
!*/*.yml
|
||||||
|
|
||||||
|
# Whitelist specific folders (uncomment as needed)
|
||||||
|
!automations/
|
||||||
|
!scripts/
|
||||||
|
!blueprints/
|
||||||
|
!esphome/
|
||||||
|
!esphome/ha-remote/
|
||||||
|
!custom_components/
|
||||||
|
# !packages/
|
||||||
|
# !themes/
|
||||||
|
# !node-red/
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# SENSITIVE FILES - always ignore these
|
||||||
|
# =============================================
|
||||||
|
secrets.yaml
|
||||||
|
known_devices.yaml
|
||||||
|
ip_bans.yaml
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Generated files and folders - always ignore
|
||||||
|
# =============================================
|
||||||
|
.storage
|
||||||
|
.cloud
|
||||||
|
.google.token
|
||||||
|
home-assistant.log
|
||||||
|
home-assistant_v2.db
|
||||||
|
home-assistant_v2.db-shm
|
||||||
|
home-assistant_v2.db-wal
|
||||||
|
|
||||||
|
# Dependencies and cache
|
||||||
|
deps/
|
||||||
|
tts/
|
||||||
|
www/
|
||||||
|
custom_components/
|
||||||
|
image/
|
||||||
|
ms365_storage/
|
||||||
|
|
||||||
|
# Pickle / session files
|
||||||
|
*.pickle
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
|
||||||
# Storage directory
|
# OS files
|
||||||
.storage/
|
.DS_Store
|
||||||
ms365_storage/
|
Thumbs.db
|
||||||
image/
|
|
||||||
.cloud/
|
|
||||||
www/
|
|
||||||
esphome/
|
|
||||||
custom_components/
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
*.log.1
|
|
||||||
*.log.fault
|
|
||||||
|
|
||||||
## Sensitive stuff
|
|
||||||
secrets.yaml
|
|
||||||
|
|||||||
1501
automations.yaml
1501
automations.yaml
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@ frontend:
|
|||||||
themes: !include_dir_merge_named themes
|
themes: !include_dir_merge_named themes
|
||||||
|
|
||||||
automation: !include automations.yaml
|
automation: !include automations.yaml
|
||||||
script: !include scripts.yaml
|
|
||||||
scene: !include scenes.yaml
|
scene: !include scenes.yaml
|
||||||
input_boolean: !include input_boolean.yaml
|
input_boolean: !include input_boolean.yaml
|
||||||
http:
|
http:
|
||||||
|
|||||||
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
|
||||||
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
|
||||||
83
esphome/declan-a1-camera.yaml
Normal file
83
esphome/declan-a1-camera.yaml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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
|
||||||
|
#cpu_frequency: 240MHZ
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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: 640x480
|
||||||
|
jpeg_quality: 20
|
||||||
|
vertical_flip: False
|
||||||
|
horizontal_mirror: False
|
||||||
|
max_framerate: 22 fps
|
||||||
|
idle_framerate: 0.1 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
|
||||||
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);
|
||||||
51
esphome/ha-remote-1.yaml
Normal file
51
esphome/ha-remote-1.yaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
substitutions:
|
||||||
|
device_name: ha-family-room-remote
|
||||||
|
friendly_name: "Family Room HA Remote"
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${device_name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
on_boot:
|
||||||
|
priority: -10
|
||||||
|
then:
|
||||||
|
- light.turn_on: ha_remote_backlight
|
||||||
|
- lvgl.page.show: home
|
||||||
|
- lambda: |-
|
||||||
|
id(last_activity_ms) = millis();
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32-s3-devkitc-1
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: WARN
|
||||||
|
|
||||||
|
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
|
||||||
|
power_save_mode: NONE
|
||||||
|
fast_connect: true
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "${friendly_name} Fallback"
|
||||||
|
password: !secret fallback_password
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
packages:
|
||||||
|
base: !include ha-remote/ha-remote-1.base.yaml
|
||||||
|
bindings: !include ha-remote/ha-remote-1.bindings.yaml
|
||||||
|
ui: !include ha-remote/ha-remote-1.ui.yaml
|
||||||
129
esphome/ha-remote/ha-remote-1.base.yaml
Normal file
129
esphome/ha-remote/ha-remote-1.base.yaml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
psram:
|
||||||
|
mode: octal
|
||||||
|
speed: 80MHz
|
||||||
|
|
||||||
|
i2c:
|
||||||
|
sda: 8
|
||||||
|
scl: 9
|
||||||
|
|
||||||
|
# CH422G I/O expander (Waveshare uses it for LCD reset/backlight/touch reset)
|
||||||
|
ch422g:
|
||||||
|
- id: ch422g_hub
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
update_interval: 120ms
|
||||||
|
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
|
||||||
|
# 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.
|
||||||
|
|
||||||
311
esphome/ha-remote/ha-remote-1.bindings.yaml
Normal file
311
esphome/ha-remote/ha-remote-1.bindings.yaml
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
lv_obj_set_style_text_color(btn2, 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));
|
||||||
|
lv_obj_set_style_text_color(btn2, 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
- 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));
|
||||||
|
|
||||||
2698
esphome/ha-remote/ha-remote-1.ui.yaml
Normal file
2698
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: 800x600
|
||||||
|
jpeg_quality: 15
|
||||||
|
vertical_flip: False
|
||||||
|
horizontal_mirror: False
|
||||||
|
max_framerate: 20 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"
|
||||||
78
esphome/polly-a1-mini-camera.yaml
Normal file
78
esphome/polly-a1-mini-camera.yaml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
esphome:
|
||||||
|
name: polly-a1-mini-camera
|
||||||
|
friendly_name: polly-a1-mini-camera
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32dev
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
# minimum_chip_revision: 3.1
|
||||||
|
|
||||||
|
# Enable logging
|
||||||
|
logger:
|
||||||
|
|
||||||
|
# Enable Home Assistant API
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: "iLF5QCgTVCwgaA5vjTGez03Dfjvns8JvkcwmUN4NXR8="
|
||||||
|
|
||||||
|
ota:
|
||||||
|
- platform: esphome
|
||||||
|
password: "58f86a5e60abe60886cec2f65a1d3da5"
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: !secret wifi_ssid
|
||||||
|
password: !secret wifi_password
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "Polly-A1-Mini-Camera"
|
||||||
|
password: "7so1xpjNMIMr"
|
||||||
|
|
||||||
|
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: 800x600
|
||||||
|
jpeg_quality: 20
|
||||||
|
#horizontal_mirror: False
|
||||||
|
max_framerate: 20 fps
|
||||||
|
idle_framerate: 0.05 fps
|
||||||
|
|
||||||
|
# Image settings
|
||||||
|
name: esp32-polly-a1-mini-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: "polly-a1-mini-cam-flash"
|
||||||
|
pin: 4
|
||||||
|
|
||||||
5133
ha-entities.txt
Normal file
5133
ha-entities.txt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user