From c6dd8ea4b5019a374cf384897188549e156762a4 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 21 Jun 2025 19:59:43 -0400 Subject: [PATCH] Convert to SGP41 sensor SGP30's were too unreliable, so swap to the SGP41 and its adjusted configurations. We leverage the same algorithm and values as the AirGradient One to ensure consistency. --- microenv.yaml | 174 +++++++++++++++----------------------------------- 1 file changed, 52 insertions(+), 122 deletions(-) diff --git a/microenv.yaml b/microenv.yaml index cbd6bb3..e8a9e08 100644 --- a/microenv.yaml +++ b/microenv.yaml @@ -1,7 +1,7 @@ --- ############################################################################### -# SuperSensor v2.0 ESPHome configuration +# MicroEnv v1.0 ESPHome configuration ############################################################################### # # Copyright (C) 2025 Joshua M. Boniface @@ -85,37 +85,62 @@ i2c: scan: true sensor: - - platform: sgp30 - address: 0x58 - eco2: - name: "SGP30 eCO2" - id: sgp30_eco2 - accuracy_decimals: 1 - icon: mdi:molecule-co2 + - platform: sgp4x + voc: + name: "SGP41 VOC Index" + id: sgp41_voc_index + accuracy_decimals: 0 + icon: mdi:molecule filters: - - sliding_window_moving_average: - window_size: 20 - send_every: 1 - tvoc: - name: "SGP30 TVOC" - id: sgp30_tvoc - accuracy_decimals: 1 + - sliding_window_moving_average: # We take a reading every 15 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:molecule filters: - sliding_window_moving_average: - window_size: 20 - send_every: 1 - eco2_baseline: - name: "SGP30 Baseline eCO2" - id: sgp30_baseline_ec02 - tvoc_baseline: - name: "SGP30 Baseline TVOC" - id: sgp30_baseline_tvoc + window_size: 12 + send_every: 3 compensation: temperature_source: sht45_temperature humidity_source: sht45_humidity - store_baseline: yes - update_interval: 15s + store_baseline: true + update_interval: 5s + + - platform: template + name: "SGP41 TVOC (µg/m³)" + id: sgp41_tvoc_ugm3 + 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 + + - platform: template + name: "SGP41 TVOC (ppb)" + id: sgp41_tvoc_ppb + 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 + + - platform: template + name: "SGP41 eCO2 (approx ppm)" + id: sgp41_eco2_ppm + 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 - platform: sht4x temperature: @@ -125,7 +150,7 @@ sensor: filters: - offset: !lambda return id(temperature_offset); - sliding_window_moving_average: - window_size: 20 + window_size: 4 send_every: 1 humidity: name: "SHT45 Relative Humidity" @@ -134,7 +159,7 @@ sensor: filters: - offset: !lambda return id(humidity_offset); - sliding_window_moving_average: - window_size: 20 + window_size: 4 send_every: 1 heater_max_duty: 0.0 update_interval: 15s @@ -159,50 +184,6 @@ sensor: return (b * alpha) / (a - alpha); update_interval: 15s - # IAQ Index (1-5, 5=Great)) - - platform: template - name: "IAQ Index" - icon: mdi:air-purifier - id: iaq_index - lambda: |- - int tvoc = id(sgp30_tvoc).state; - int eco2 = id(sgp30_eco2).state; - if (tvoc > 2200 || eco2 > 2000) return 1; // Bad - if (tvoc > 660 || eco2 > 1200) return 2; // Poor - if (tvoc > 220 || eco2 > 800) return 3; // Fair - if (tvoc > 65 || eco2 > 500) return 4; // Good - return 5; // Great - update_interval: 15s - - # Room Health Score (1-4, 4=Optimal) - - platform: template - name: "Room Health Score" - icon: mdi:home-thermometer - id: room_health - lambda: |- - float temp = id(sht45_temperature).state; - float rh = id(sht45_humidity).state; - int iaq = id(iaq_index).state; - - bool temp_ok = (temp >= 18 && temp <= 24); - bool hum_ok = (rh >= 30 && rh <= 70); - bool iaq_ok = (iaq >= 4); - - int conditions_met = 0; - if (temp_ok) conditions_met++; - if (hum_ok) conditions_met++; - if (iaq_ok) conditions_met++; - - if (iaq_ok && temp_ok && hum_ok) { - return 4; // Optimal: All conditions met and IAQ is excellent/good - } else if (iaq >= 3 && conditions_met >= 2) { - return 3; // Fair: IAQ is moderate and at least 2 conditions met - } else if (iaq >= 2 && conditions_met >= 1) { - return 2; // Poor: IAQ is poor and at least 1 condition met - } else { - return 1; // Bad: All conditions failed or IAQ is unhealthy - } - - platform: wifi_signal name: "WiFi Signal" update_interval: 60s @@ -228,57 +209,6 @@ text_sensor: mac_address: name: "WiFi MAC Address" - # VOC Level - - platform: template - name: "VOC Level" - icon: mdi:molecule - lambda: |- - int tvoc = id(sgp30_tvoc).state; - if (tvoc < 65) return {"Great"}; - if (tvoc < 220) return {"Good"}; - if (tvoc < 660) return {"Fair"}; - if (tvoc < 2200) return {"Poor"}; - return {"Bad"}; - update_interval: 15s - - # CO2 Level - - platform: template - name: "CO2 Level" - icon: mdi:molecule-co2 - lambda: |- - int eco2 = id(sgp30_eco2).state; - if (eco2 < 500) return {"Great"}; - if (eco2 < 800) return {"Good"}; - if (eco2 < 1200) return {"Fair"}; - if (eco2 < 2000) return {"Poor"}; - return {"Bad"}; - update_interval: 15s - - # IAQ Classification - - platform: template - name: "IAQ Classification" - icon: mdi:air-purifier - lambda: |- - int iaq = id(iaq_index).state; - if (iaq == 5) return {"Great"}; - if (iaq == 4) return {"Good"}; - if (iaq == 3) return {"Fair"}; - if (iaq == 2) return {"Poor"}; - return {"Bad"}; - update_interval: 15s - - # Room Health - - platform: template - name: "Room Health" - icon: mdi:home-thermometer - lambda: |- - int score = id(room_health).state; - if (score == 4) return {"Optimal"}; - if (score == 3) return {"Fair"}; - if (score == 2) return {"Poor"}; - return {"Bad"}; - update_interval: 15s - button: - platform: restart name: "ESP32 Restart"