Files
microenv/microenv.yaml
Joshua M. Boniface 07eefdb466 Add configurable room health settings
Port in from SuperSensor configuration.
2026-01-03 03:54:10 -05:00

563 lines
16 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
###############################################################################
# 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 0100 %
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 (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
- 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' ]