--- ############################################################################### # MicroEnv v1.0 ESPHome configuration ############################################################################### # # Copyright (C) 2025 Joshua M. Boniface # # 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 . # ############################################################################### 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' ]