Files
microenv/microenv.yaml

563 lines
16 KiB
YAML
Raw Normal View History

2025-05-27 21:20:58 -04:00
---
###############################################################################
2026-01-03 03:33:48 -05:00
# MicroEnv v2.0 ESPHome configuration
2025-05-27 21:20:58 -04:00
###############################################################################
#
# 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"
2026-01-03 03:33:48 -05:00
version: "2.0"
min_version: 2025.11.0
2025-05-27 21:20:58 -04:00
dashboard_import:
2026-01-03 03:33:48 -05:00
package_import_url: github://joshuaboniface/microenv/microenv.yaml@v2.x
2025-05-27 21:20:58 -04:00
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"
2025-05-27 21:20:58 -04:00
- 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"
2025-05-27 21:20:58 -04:00
logger:
level: INFO
2025-05-27 21:55:20 -04:00
baud_rate: 115200
2025-05-27 21:20:58 -04:00
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
2025-06-21 20:30:40 -04:00
icon: mdi:waves-arrow-up
2025-05-27 21:20:58 -04:00
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
2025-06-21 20:30:40 -04:00
icon: mdi:waves-arrow-up
2025-05-27 21:20:58 -04:00
filters:
- sliding_window_moving_average:
window_size: 12
send_every: 3
2025-05-27 21:20:58 -04:00
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
2025-06-21 20:30:40 -04:00
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
2025-06-21 20:30:40 -04:00
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
2025-07-06 03:00:14 -04:00
name: "SGP41 eCO2 (appr.)"
id: sgp41_eco2_appr
2025-06-21 20:30:40 -04:00
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
2025-05-27 21:20:58 -04:00
- 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
2025-05-27 21:20:58 -04:00
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 0100 %
if (rh_corr < 0) rh_corr = 0;
if (rh_corr > 100) rh_corr = 100;
return rh_corr;
2025-05-27 21:20:58 -04:00
- offset: !lambda return id(humidity_offset);
- sliding_window_moving_average:
window_size: 4
2025-05-27 21:20:58 -04:00
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 (0100) mapped to categories from Chemical Pollution levels below
float voc_score;
if (voc <= 200) {
voc_score = 100.0;
} else if (voc <= 400) {
// 200400: 100 → 90
voc_score = 100.0 - (voc - 200) * (10.0 / 200.0);
} else if (voc <= 600) {
// 400600: 90 → 70
voc_score = 90.0 - (voc - 400) * (20.0 / 200.0);
} else if (voc <= 1500) {
// 6001500: 70 → 40
voc_score = 70.0 - (voc - 600) * (30.0 / 900.0);
} else if (voc <= 3000) {
// 15003000: 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
2025-05-27 21:20:58 -04:00
- 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
2025-05-27 21:20:58 -04:00
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
2025-05-27 21:20:58 -04:00
- 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
2025-05-27 21:20:58 -04:00
- platform: template
name: "Humidity Offset"
id: humidity_offset_setter
min_value: -50
max_value: 50
2025-05-27 21:20:58 -04:00
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' ]