563 lines
16 KiB
YAML
563 lines
16 KiB
YAML
---
|
||
|
||
###############################################################################
|
||
# MicroEnv v1.0 ESPHome configuration
|
||
###############################################################################
|
||
#
|
||
# Copyright (C) 2025 Joshua M. Boniface <joshua@boniface.me>
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published by
|
||
# the Free Software Foundation, version 3.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
#
|
||
###############################################################################
|
||
|
||
esphome:
|
||
name: microenv
|
||
name_add_mac_suffix: true
|
||
friendly_name: "MicroEnv Sensor"
|
||
project:
|
||
name: "Joshua Boniface.microenv"
|
||
version: "1.0"
|
||
min_version: 2025.11.0
|
||
|
||
dashboard_import:
|
||
package_import_url: github://joshuaboniface/microenv/microenv.yaml@v1.x
|
||
|
||
esp32:
|
||
board: esp32-c3-devkitm-1
|
||
variant: esp32c3
|
||
framework:
|
||
type: esp-idf
|
||
|
||
preferences:
|
||
flash_write_interval: 15sec
|
||
|
||
globals:
|
||
- id: temperature_offset
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "0.0"
|
||
|
||
- id: humidity_offset
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "0.0"
|
||
|
||
#
|
||
# Room Health settings
|
||
#
|
||
- id: room_health_temperature_min
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "21.0"
|
||
|
||
- id: room_health_temperature_max
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "24.0"
|
||
|
||
- id: room_health_temperature_penalty
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "10.0"
|
||
|
||
- id: room_health_humidity_min
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "40.0"
|
||
|
||
- id: room_health_humidity_max
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "60.0"
|
||
|
||
- id: room_health_humidity_penalty
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "5.0"
|
||
|
||
- id: room_health_voc_weight
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "0.4"
|
||
|
||
- id: room_health_temperature_weight
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "0.3"
|
||
|
||
- id: room_health_humidity_weight
|
||
type: float
|
||
restore_value: true
|
||
initial_value: "0.3"
|
||
|
||
logger:
|
||
level: INFO
|
||
baud_rate: 115200
|
||
|
||
api:
|
||
reboot_timeout: 15min
|
||
|
||
ota:
|
||
platform: esphome
|
||
|
||
web_server:
|
||
port: 80
|
||
|
||
captive_portal:
|
||
|
||
mdns:
|
||
disabled: false
|
||
|
||
wifi:
|
||
ap: {}
|
||
domain: ""
|
||
output_power: 8.5dB
|
||
reboot_timeout: 15min
|
||
power_save_mode: none
|
||
|
||
i2c:
|
||
- id: i2c_bus
|
||
sda: GPIO21
|
||
scl: GPIO20
|
||
scan: true
|
||
|
||
sensor:
|
||
- platform: sgp4x
|
||
voc:
|
||
name: "SGP41 VOC Index"
|
||
id: sgp41_voc_index
|
||
accuracy_decimals: 0
|
||
icon: mdi:waves-arrow-up
|
||
filters:
|
||
- sliding_window_moving_average: # We take a reading every 5 seconds, but calculate the sliding
|
||
window_size: 12 # average over 12 readings i.e. 60 seconds/1 minute to normalize
|
||
send_every: 3 # brief spikes while still sending a value every 15 seconds.
|
||
nox:
|
||
name: "SGP41 NOx Index"
|
||
id: sgp41_nox_index
|
||
accuracy_decimals: 0
|
||
icon: mdi:waves-arrow-up
|
||
filters:
|
||
- sliding_window_moving_average:
|
||
window_size: 12
|
||
send_every: 3
|
||
compensation:
|
||
temperature_source: sht45_temperature
|
||
humidity_source: sht45_humidity
|
||
store_baseline: true
|
||
update_interval: 5s
|
||
|
||
- platform: template
|
||
name: "SGP41 TVOC (µg/m³)"
|
||
id: sgp41_tvoc_ugm3
|
||
icon: mdi:molecule
|
||
lambda: |-
|
||
float i = id(sgp41_voc_index).state;
|
||
if (i < 1) return NAN;
|
||
float tvoc = (log(501.0 - i) - 6.24) * -878.53;
|
||
return tvoc;
|
||
unit_of_measurement: "µg/m³"
|
||
accuracy_decimals: 0
|
||
update_interval: 15s
|
||
|
||
- platform: template
|
||
name: "SGP41 TVOC (ppb)"
|
||
id: sgp41_tvoc_ppb
|
||
icon: mdi:molecule
|
||
lambda: |-
|
||
float tvoc_ugm3 = id(sgp41_tvoc_ugm3).state;
|
||
float tvoc_ppm = tvoc_ugm3 * 0.436; // ppb estimated using isobutylene MW (56.1 g/mol)
|
||
return tvoc_ppm;
|
||
unit_of_measurement: "ppb"
|
||
accuracy_decimals: 0
|
||
update_interval: 15s
|
||
|
||
- platform: template
|
||
name: "SGP41 eCO2 (appr.)"
|
||
id: sgp41_eco2_appr
|
||
icon: mdi:molecule-co2
|
||
lambda: |-
|
||
float tvoc_ppb = id(sgp41_tvoc_ppb).state;
|
||
float eco2_ppm = 400.0 + 1.5 * tvoc_ppb;
|
||
if (eco2_ppm > 2000) eco2_ppm = 2000;
|
||
return eco2_ppm;
|
||
unit_of_measurement: "ppm"
|
||
accuracy_decimals: 0
|
||
update_interval: 15s
|
||
|
||
- platform: sht4x
|
||
temperature:
|
||
name: "SHT45 Temperature"
|
||
id: sht45_temperature
|
||
accuracy_decimals: 1
|
||
filters:
|
||
- offset: !lambda return id(temperature_offset);
|
||
- sliding_window_moving_average:
|
||
window_size: 4
|
||
send_every: 1
|
||
humidity:
|
||
name: "SHT45 Relative Humidity"
|
||
id: sht45_humidity
|
||
accuracy_decimals: 1
|
||
filters:
|
||
- lambda: |-
|
||
// Grab measured and corrected temperatures
|
||
float t_meas = id(sht45_temperature).state - id(temperature_offset);
|
||
float t_corr = id(sht45_temperature).state;
|
||
float rh_meas = x;
|
||
|
||
// Compute saturation vapor pressures (Magnus formula)
|
||
auto es = [](float T) { return 6.112 * exp((17.62 * T) / (243.12 + T)); };
|
||
float rh_corr = rh_meas * es(t_meas) / es(t_corr);
|
||
|
||
// Clamp to 0–100 %
|
||
if (rh_corr < 0) rh_corr = 0;
|
||
if (rh_corr > 100) rh_corr = 100;
|
||
return rh_corr;
|
||
- offset: !lambda return id(humidity_offset);
|
||
- sliding_window_moving_average:
|
||
window_size: 4
|
||
send_every: 1
|
||
heater_max_duty: 0.0
|
||
update_interval: 15s
|
||
|
||
- platform: absolute_humidity
|
||
name: "SHT45 Absolute Humidity"
|
||
temperature: sht45_temperature
|
||
humidity: sht45_humidity
|
||
id: sht45_absolute_humidity
|
||
|
||
- platform: template
|
||
name: "SHT45 Dew Point"
|
||
icon: mdi:thermometer-water
|
||
id: sht45_dew_point
|
||
unit_of_measurement: "°C"
|
||
lambda: |-
|
||
float temp = id(sht45_temperature).state;
|
||
float rh = id(sht45_humidity).state;
|
||
if (isnan(temp) || isnan(rh)) return NAN;
|
||
float a = 17.27, b = 237.7;
|
||
float alpha = ((a * temp) / (b + temp)) + log(rh / 100.0);
|
||
return (b * alpha) / (a - alpha);
|
||
update_interval: 15s
|
||
|
||
- platform: template
|
||
name: "Room Health Score"
|
||
id: room_health_score
|
||
unit_of_measurement: "%"
|
||
icon: mdi:home-heart
|
||
lambda: |-
|
||
float voc = id(sgp41_tvoc_ppb).state;
|
||
if (isnan(voc) || voc < 1) voc = 1;
|
||
float temp = id(sht45_temperature).state;
|
||
float humidity = id(sht45_humidity).state;
|
||
|
||
float temp_min = id(room_health_temperature_min);
|
||
float temp_max = id(room_health_temperature_max);
|
||
float temp_penalty = id(room_health_temperature_penalty);
|
||
float humid_min = id(room_health_humidity_min);
|
||
float humid_max = id(room_health_humidity_max);
|
||
float humid_penalty = id(room_health_humidity_penalty);
|
||
float voc_weight = id(room_health_voc_weight);
|
||
float temp_weight = id(room_health_temperature_weight);
|
||
float humid_weight = id(room_health_humidity_weight);
|
||
|
||
// VOC score (0–100) mapped to categories from Chemical Pollution levels below
|
||
float voc_score;
|
||
if (voc <= 200) {
|
||
voc_score = 100.0;
|
||
} else if (voc <= 400) {
|
||
// 200–400: 100 → 90
|
||
voc_score = 100.0 - (voc - 200) * (10.0 / 200.0);
|
||
} else if (voc <= 600) {
|
||
// 400–600: 90 → 70
|
||
voc_score = 90.0 - (voc - 400) * (20.0 / 200.0);
|
||
} else if (voc <= 1500) {
|
||
// 600–1500: 70 → 40
|
||
voc_score = 70.0 - (voc - 600) * (30.0 / 900.0);
|
||
} else if (voc <= 3000) {
|
||
// 1500–3000: 40 → 0
|
||
voc_score = 40.0 - (voc - 1500) * (40.0 / 1500.0);
|
||
} else {
|
||
voc_score = 0.0;
|
||
}
|
||
|
||
// Temperature score
|
||
float temp_score = 100;
|
||
if (temp < temp_min) temp_score = 100 - (temp_min - temp) * temp_penalty;
|
||
else if (temp > temp_max) temp_score = 100 - (temp - temp_max) * temp_penalty;
|
||
if (temp_score < 0) temp_score = 0;
|
||
|
||
// Humidity score
|
||
float humidity_score = 100;
|
||
if (humidity < humid_min) humidity_score = 100 - (humid_min - humidity) * humid_penalty;
|
||
else if (humidity > humid_max) humidity_score = 100 - (humidity - humid_max) * humid_penalty;
|
||
if (humidity_score < 0) humidity_score = 0;
|
||
|
||
// Weighted average
|
||
float total_weights = voc_weight + temp_weight + humid_weight;
|
||
if (total_weights <= 0) total_weights = 1.0;
|
||
voc_weight /= total_weights;
|
||
temp_weight /= total_weights;
|
||
humid_weight /= total_weights;
|
||
float overall_score = (voc_score * voc_weight + temp_score * temp_weight + humidity_score * humid_weight);
|
||
|
||
return (int) round(overall_score);
|
||
update_interval: 15s
|
||
|
||
- platform: wifi_signal
|
||
name: "WiFi Signal"
|
||
update_interval: 60s
|
||
entity_category: diagnostic
|
||
|
||
- platform: uptime
|
||
name: "Uptime"
|
||
update_interval: 60s
|
||
entity_category: diagnostic
|
||
|
||
text_sensor:
|
||
- platform: version
|
||
name: "ESPHome Version"
|
||
entity_category: diagnostic
|
||
|
||
- platform: wifi_info
|
||
ip_address:
|
||
name: "WiFi IP Address"
|
||
ssid:
|
||
name: "WiFi SSID"
|
||
bssid:
|
||
name: "WiFi BSSID"
|
||
mac_address:
|
||
name: "WiFi MAC Address"
|
||
|
||
- platform: template
|
||
name: "Chemical Pollution"
|
||
id: sgp41_chemical_pollution
|
||
icon: mdi:molecule
|
||
lambda: |-
|
||
float voc = id(sgp41_tvoc_ppb).state;
|
||
if (isnan(voc) || voc < 1) return {"Unknown"};
|
||
else if (voc <= 200) return {"Excellent"};
|
||
else if (voc <= 400) return {"Good"};
|
||
else if (voc <= 600) return {"Moderate"};
|
||
else if (voc <= 1500) return {"Unhealthy"};
|
||
else return {"Hazardous"};
|
||
update_interval: 15s
|
||
|
||
- platform: template
|
||
name: "Room Health"
|
||
id: room_health_text
|
||
icon: mdi:home-heart
|
||
lambda: |-
|
||
float score = id(room_health_score).state;
|
||
if (score < 0) return {"Unknown"};
|
||
else if (score >= 100.0) return {"Excellent"};
|
||
else if (score >= 95.0) return {"Great"};
|
||
else if (score >= 90.0) return {"Good"};
|
||
else if (score >= 80.0) return {"Fair"};
|
||
else if (score >= 60.0) return {"Poor"};
|
||
else return {"Bad"};
|
||
update_interval: 15s
|
||
|
||
button:
|
||
- platform: restart
|
||
name: "ESP32 Restart"
|
||
icon: mdi:power-cycle
|
||
entity_category: diagnostic
|
||
|
||
- platform: factory_reset
|
||
name: "ESP32 Factory Reset"
|
||
icon: mdi:restart-alert
|
||
entity_category: diagnostic
|
||
|
||
number:
|
||
# Temperature offset:
|
||
# A calibration from -30 to +10 for the temperature sensor
|
||
- platform: template
|
||
name: "Temperature Offset"
|
||
id: temperature_offset_setter
|
||
min_value: -30
|
||
max_value: 10
|
||
step: 0.1
|
||
lambda: |-
|
||
return id(temperature_offset);
|
||
set_action:
|
||
then:
|
||
- globals.set:
|
||
id: temperature_offset
|
||
value: !lambda 'return float(x);'
|
||
|
||
# Humidity offset:
|
||
# A calibration from -50 to +50 for the humidity sensor
|
||
- platform: template
|
||
name: "Humidity Offset"
|
||
id: humidity_offset_setter
|
||
min_value: -50
|
||
max_value: 50
|
||
step: 0.1
|
||
lambda: |-
|
||
return id(humidity_offset);
|
||
set_action:
|
||
then:
|
||
- globals.set:
|
||
id: humidity_offset
|
||
value: !lambda 'return float(x);'
|
||
|
||
# Room Health Calibration Values
|
||
# These values allow the user to tweak the values of the room health calculation
|
||
- platform: template
|
||
name: "Room Health Min Temperature"
|
||
id: room_health_temperature_min_setter
|
||
min_value: 15
|
||
max_value: 30
|
||
step: 0.5
|
||
lambda: |-
|
||
return id(room_health_temperature_min);
|
||
set_action:
|
||
then:
|
||
- globals.set:
|
||
id: room_health_temperature_min
|
||
value: !lambda 'return float(x);'
|
||
- platform: template
|
||
name: "Room Health Max Temperature"
|
||
id: room_health_temperature_max_setter
|
||
min_value: 15
|
||
max_value: 30
|
||
step: 0.5
|
||
lambda: |-
|
||
return id(room_health_temperature_max);
|
||
set_action:
|
||
then:
|
||
- globals.set:
|
||
id: room_health_temperature_max
|
||
value: !lambda 'return float(x);'
|
||
- platform: template
|
||
name: "Room Health Temperature Penalty"
|
||
id: room_health_temperature_penalty_setter
|
||
min_value: 1
|
||
max_value: 20
|
||
step: 1
|
||
lambda: |-
|
||
return int(id(room_health_temperature_penalty));
|
||
set_action:
|
||
then:
|
||
- globals.set:
|
||
id: room_health_temperature_penalty
|
||
value: !lambda 'return float(x);'
|
||
- platform: template
|
||
name: "Room Health Min Humidity"
|
||
id: room_health_humidity_min_setter
|
||
min_value: 20
|
||
max_value: 80
|
||
step: 1.0
|
||
lambda: |-
|
||
return id(room_health_humidity_min);
|
||
set_action:
|
||
then:
|
||
- globals.set:
|
||
id: room_health_humidity_min
|
||
value: !lambda 'return float(x);'
|
||
- platform: template
|
||
name: "Room Health Max Humidity"
|
||
id: room_health_humidity_max_setter
|
||
min_value: 20
|
||
max_value: 80
|
||
step: 1.0
|
||
lambda: |-
|
||
return id(room_health_humidity_max);
|
||
set_action:
|
||
then:
|
||
- globals.set:
|
||
id: room_health_humidity_max
|
||
value: !lambda 'return float(x);'
|
||
- platform: template
|
||
name: "Room Health Humidity Penalty"
|
||
id: room_health_humidity_penalty_setter
|
||
min_value: 1
|
||
max_value: 10
|
||
step: 1
|
||
lambda: |-
|
||
return int(id(room_health_humidity_penalty));
|
||
set_action:
|
||
then:
|
||
- globals.set:
|
||
id: room_health_humidity_penalty
|
||
value: !lambda 'return float(x);'
|
||
- platform: template
|
||
name: "Room Health VOC Weight"
|
||
id: room_health_voc_weight_setter
|
||
min_value: 0.00
|
||
max_value: 1.00
|
||
step: 0.01
|
||
lambda: |-
|
||
return id(room_health_voc_weight);
|
||
set_action:
|
||
- if:
|
||
condition:
|
||
lambda: |-
|
||
float total = x + id(room_health_temperature_weight) + id(room_health_humidity_weight);
|
||
return (total > 0.0) && (total <= 1.0);
|
||
then:
|
||
- globals.set:
|
||
id: room_health_voc_weight
|
||
value: !lambda 'return float(x);'
|
||
else:
|
||
- logger.log:
|
||
format: "Rejected VOC weight %.2f (total would be out of range: must be > 0.0 and ≤ 1.0)"
|
||
args: [ 'x' ]
|
||
- platform: template
|
||
name: "Room Health Temperature Weight"
|
||
id: room_health_temperature_weight_setter
|
||
min_value: 0.00
|
||
max_value: 1.00
|
||
step: 0.01
|
||
lambda: |-
|
||
return id(room_health_temperature_weight);
|
||
set_action:
|
||
- if:
|
||
condition:
|
||
lambda: |-
|
||
float total = x + id(room_health_voc_weight) + id(room_health_humidity_weight);
|
||
return (total > 0.0) && (total <= 1.0);
|
||
then:
|
||
- globals.set:
|
||
id: room_health_temperature_weight
|
||
value: !lambda 'return float(x);'
|
||
else:
|
||
- logger.log:
|
||
format: "Rejected Temperature weight %.2f (total would be out of range: must be > 0.0 and ≤ 1.0)"
|
||
args: [ 'x' ]
|
||
- platform: template
|
||
name: "Room Health Humidity Weight"
|
||
id: room_health_humidity_weight_setter
|
||
min_value: 0.00
|
||
max_value: 1.00
|
||
step: 0.01
|
||
lambda: |-
|
||
return id(room_health_humidity_weight);
|
||
set_action:
|
||
- if:
|
||
condition:
|
||
lambda: |-
|
||
float total = x + id(room_health_temperature_weight) + id(room_health_voc_weight);
|
||
return (total > 0.0) && (total <= 1.0);
|
||
then:
|
||
- globals.set:
|
||
id: room_health_humidity_weight
|
||
value: !lambda 'return float(x);'
|
||
else:
|
||
- logger.log:
|
||
format: "Rejected Humidity weight %.2f (total would be out of range: must be > 0.0 and ≤ 1.0)"
|
||
args: [ 'x' ]
|
||
|