Switch to SGP41 air quality sensor
My testing with the SGP30 sensor proved fruitless, as various sensors would mis-report (under- and over-) levels and generally not be consistent with each other or reality. These sensors are simply too flaky with zero consistency. Instead, switch to the SGP41, which so far seems more robust and is used in other tools like the AirGradient One. We leverage their calculations for VOC Index -> VOC levels, as well as a generalized isobutylene-based eCO2 calculation.
This commit is contained in:
111
README.md
111
README.md
@ -42,15 +42,15 @@ and [my update post on version 2.0](https://www.boniface.me/the-supersensor-2.0)
|
|||||||
|
|
||||||
## Major Changes from 1.x
|
## Major Changes from 1.x
|
||||||
|
|
||||||
1. Replaced the Bosch BME680 with the Sensirion SHT45 and Sensirion SGP30.
|
1. Replaced the Bosch BME680 with the Sensirion SHT45 and Sensirion SGP41.
|
||||||
|
|
||||||
The BME680 proved to be woefully unreliable in my testing. Temperature was fairly accurate (internal heating and offset notwithstanding),
|
The BME680 proved to be woefully unreliable in my testing. Temperature was fairly accurate (internal heating and offset notwithstanding),
|
||||||
but humidity was wildly off of what other thermometers/hydrometers would report. In addition, the AQ functionality of the sensor was a
|
but humidity was wildly off of what other thermometers/hydrometers would report. In addition, the AQ functionality of the sensor was a
|
||||||
source of much frustration and I was never able to get it to work reliably, either with the official BSEC library or with my own attempts
|
source of much frustration and I was never able to get it to work reliably, either with the official BSEC library or with my own attempts
|
||||||
at self-configuration.
|
at self-configuration.
|
||||||
|
|
||||||
Thus, this sensor has been replaced with two Sensirion sensors which in my experience so far have been much more reliable and consistent,
|
Thus, this sensor has been replaced with two Sensirion sensors which in my experience so far have been much more reliable and consistent.
|
||||||
and the cost difference is negligible.
|
There is a slight cost increase due to these sensors, but not signfigant enough to outweigh the benefit of reliable monitoring they confer.
|
||||||
|
|
||||||
2. Replaced the SR602 PIR sensor with the AM312 PIR sensor.
|
2. Replaced the SR602 PIR sensor with the AM312 PIR sensor.
|
||||||
|
|
||||||
@ -64,15 +64,12 @@ and [my update post on version 2.0](https://www.boniface.me/the-supersensor-2.0)
|
|||||||
3. Completely redesigned the custom PCB around the above sensor changes, which is now more compact in a 50x55mm almost-square configuration.
|
3. Completely redesigned the custom PCB around the above sensor changes, which is now more compact in a 50x55mm almost-square configuration.
|
||||||
|
|
||||||
4. Significantly cleaned up the ESPHome configuration, to support the above sensors and remove a lot of cruft that was caused by the BME680.
|
4. Significantly cleaned up the ESPHome configuration, to support the above sensors and remove a lot of cruft that was caused by the BME680.
|
||||||
This includes a new set of custom AQ calculations based on the SGP30 and SHT45 sensors that, while not necessarily following the full EPA
|
|
||||||
IAQI spec, should still give a reasonable view of the air quality conditions of an interior room and not deviate wildly and nonsensically
|
|
||||||
like the BME680 did. Details of the calculation are provided below.
|
|
||||||
|
|
||||||
## Parts List
|
## Parts List
|
||||||
|
|
||||||
| Qty | Component | Cost (2025/05 CAD, ex. shipping) | Links |
|
| Qty | Component | Cost (2025/05 CAD, ex. shipping) | Links |
|
||||||
|-------|--------------------|----------------------------------|-------|
|
|-------|--------------------|----------------------------------|-------|
|
||||||
| 1 | GY-SGP30 | $5.73 | [AliExpress](https://www.aliexpress.com/item/1005008473372972.html) |
|
| 1 | GY-SGP41 | $11.08 | [AliExpress](https://www.aliexpress.com/item/1005006746827606.html) |
|
||||||
| 1 | GY-SHT45 | $5.67 | [AliExpress](https://www.aliexpress.com/item/1005008175340220.html)* |
|
| 1 | GY-SHT45 | $5.67 | [AliExpress](https://www.aliexpress.com/item/1005008175340220.html)* |
|
||||||
| 1 | SR602 | $0.81 | [AliExpress](https://www.aliexpress.com/item/1005001572550300.html) |
|
| 1 | SR602 | $0.81 | [AliExpress](https://www.aliexpress.com/item/1005001572550300.html) |
|
||||||
| 1 | TSL2591 | $4.59 | [AliExpress](https://www.aliexpress.com/item/1005008619462097.html) |
|
| 1 | TSL2591 | $4.59 | [AliExpress](https://www.aliexpress.com/item/1005008619462097.html) |
|
||||||
@ -84,7 +81,7 @@ and [my update post on version 2.0](https://www.boniface.me/the-supersensor-2.0)
|
|||||||
| 1 | Female pin header† | $1.59 ($15.99/10) | [Amazon](https://www.amazon.ca/dp/B08CMNRXJ1) |
|
| 1 | Female pin header† | $1.59 ($15.99/10) | [Amazon](https://www.amazon.ca/dp/B08CMNRXJ1) |
|
||||||
| 1 | Custom PCB (JLC) | $0.69 ($6.89/10) | [GitHub](https://github.com/joshuaboniface/supersensor) |
|
| 1 | Custom PCB (JLC) | $0.69 ($6.89/10) | [GitHub](https://github.com/joshuaboniface/supersensor) |
|
||||||
| 1 | 3D Printed case | $?.??‡ | [GitHub](https://github.com/joshuaboniface/supersensor) |
|
| 1 | 3D Printed case | $?.??‡ | [GitHub](https://github.com/joshuaboniface/supersensor) |
|
||||||
| **TOTAL** | | **$33.64** | |
|
| **TOTAL** | | **$38.99** | |
|
||||||
|
|
||||||
`*` Ensure you select the correct device on the page as it shows multiple options.
|
`*` Ensure you select the correct device on the page as it shows multiple options.
|
||||||
|
|
||||||
@ -112,17 +109,19 @@ SuperSensors in a single room and only want one to respond to voice commands.
|
|||||||
If enabled (the default), when overall presence is detected, the LEDs will
|
If enabled (the default), when overall presence is detected, the LEDs will
|
||||||
glow "white" at 15% power to signal presence.
|
glow "white" at 15% power to signal presence.
|
||||||
|
|
||||||
### Temperature Offset (selector, -10 to +5 @ 0.1, -5 default)
|
### Temperature Offset (selector, -30 to +10 @ 0.1, -5 default)
|
||||||
|
|
||||||
Allows calibration of the SHT45 temperature sensor with an offset from -10 to +5
|
Allows calibration of the SHT45 temperature sensor with an offset from -30 to +10
|
||||||
degrees C. Useful if the sensor is misreporting actual ambient tempreatures. Due
|
degrees C. Useful if the sensor is misreporting actual ambient tempreatures. Due
|
||||||
to internal heating of the SHT45 by the ESP32, this defaults to -5; further
|
to internal heating of the SHT45 by the ESP32, this defaults to -5; further
|
||||||
calibration may be needed for your sensors and environment.
|
calibration may be needed for your sensors and environment based on an external
|
||||||
|
reference.
|
||||||
|
|
||||||
### Humidity Offset (selector, -10 to +10 @ 0.1)
|
### Humidity Offset (selector, -20 to +20 @ 0.1)
|
||||||
|
|
||||||
Allows calibration of the SHT45 humidity sensor with an offset from -10 to +10
|
Allows calibration of the SHT45 humidity sensor with an offset from -10 to +10
|
||||||
percent relative humidity. Useful if the sensor is misreporting actual humidity.
|
percent relative humidity. Useful if the sensor is misreporting actual humidity
|
||||||
|
based on an external reference.
|
||||||
|
|
||||||
### PIR Hold Time (selector, 0 to +60 @ 5, 0 default)
|
### PIR Hold Time (selector, 0 to +60 @ 5, 0 default)
|
||||||
|
|
||||||
@ -230,70 +229,28 @@ is likely not useful.
|
|||||||
|
|
||||||
## AQ Details
|
## AQ Details
|
||||||
|
|
||||||
The SuperSensor 2.x provides 2 base air quality sensors (numeric), from which
|
The SuperSensor 2.0 features an SGP41 air quality sensor by Sensirion. This is a powerful AQ
|
||||||
4 human-readable text sensors are derived.
|
sensor which powers several commercial devices including the AirGradient One, which gave
|
||||||
|
us a lot of our configuration via their sharing of algorithms.
|
||||||
|
|
||||||
The goal of these sensors is to track general comfort and livability in a
|
The sensor provides two base readings: a VOC Index, and a NOx Index. These values are both
|
||||||
room, not specific contaminants or conditions. Because the SGP30 can only
|
floating references centered at 100 (VOC) and 1 (NOx), where that value represents "normal"
|
||||||
track TVOC and eCO2, we do not track particulates, CO, NOx, or CH2O, all
|
air over the previous 24 hours. These sensors are very useful for any sort of quick-change
|
||||||
of which are required for a full EPA (I)AQI score. This means the best
|
automations, e.g. turn on a fan if levels spike due to cooking.
|
||||||
we can do is approximate (I)AQI roughly, and since a scale of 0-500 based
|
|
||||||
on approximations seems pointless, I went with much simpler 1-4/5 scores
|
|
||||||
instead. I feel this does a good enough job to be useful for 99% of rooms.
|
|
||||||
|
|
||||||
We also cannot really debate whether the BME680 is actually any more accurate
|
In addition, we leverage AirGradient's published forumulas to convert the VOC index into
|
||||||
in this regard, since their algorithms are proprietary and all that is exposed
|
actual VOC quantities, in both µg/m³ and ppb. While this may drift due to the sensor's regular
|
||||||
normally is a single resistance value, so in my opinion this is actually
|
internal recalibration, I feel that following what AirGradient does is sufficient enough
|
||||||
superior to that sensor anyways with two discrete datapoints (versus one),
|
for any real-world home usage. Further, we use a very rough conversion of the aforementioned
|
||||||
even if it does still seem limited when compared to dedicated AQ sensors.
|
VOC quantity into an eCO2 reading, using Isobutylene as a reference gas. These sensors are
|
||||||
And that is to say nothing of the issues with that sensor (constantly climbing
|
more useful for display purposes, to show the current levels in a room in a dashboard or
|
||||||
IAQ values over time, poor calibration, etc.).
|
other such place, for human consumption. Note that no such conversions are done for NOx as
|
||||||
|
there are no (that I can find) published empirical calculations for this conversion, unlike
|
||||||
|
for VOCs via AirGradient.
|
||||||
|
|
||||||
### Base Numeric Values
|
Note however that like all MOx sensors, the SGP41 does not differentiate gasses, and as
|
||||||
|
such cannot tell the difference between normal, everyday natural VOCs like those in
|
||||||
#### IAQ Index (1-5)
|
breath or from e.g. ripening fruit, and dangerous VOCs from e.g. construction materials.
|
||||||
|
These should be used only as a general indication of air quality over short periods, rather
|
||||||
The IAQ index is calculated based on the TVOC and eCO2 values from the SGP30
|
than an absolute reference over long periods (much to my own frustration but inevitable
|
||||||
sensor, to provide 5 levels of air quality. This corresponds approximately
|
begruding acceptance).
|
||||||
to the levels provided by the BME680 (0-50, 50-100, 100-200, 200-300, 300+).
|
|
||||||
|
|
||||||
5 is "great": the TVOC is <65 ppb and the eCO2 is <600 ppm.
|
|
||||||
4 is "good": the TVOC is 65-220 ppb or the eCO2 is 600-800 ppm.
|
|
||||||
3 is "fair": the TVOC is 220-660 ppb or the eCO2 is 800-1200 ppm.
|
|
||||||
2 is "poor": the TVOC is 660-2200 ppb or the eCO2 is 1200-2000 ppm.
|
|
||||||
1 is "bad": the TVOC is >2200 ppb or the eCO2 is >2000 ppm.
|
|
||||||
|
|
||||||
#### Room Health Score (1-4)
|
|
||||||
|
|
||||||
The Room Health Score is calculated based on the IAQ, temperature, and humidity,
|
|
||||||
and is designed to show how "nice" a room is to be in. Generally a 4 is a nice
|
|
||||||
place to be, especially for someone with respiratory issues like myself, and lower
|
|
||||||
scores indicate more deviations from the norms or poor IAQ.
|
|
||||||
|
|
||||||
4 is "optimal": IAQ is >= 4 ("great" or "good"), temperature is between 18C and 24C, and humidity is between 40% and 60%.
|
|
||||||
3 is "fair": One of the above is not true, and IAQ is >= 3 ("fair").
|
|
||||||
2 is "poor": Two of the above are not true, and IAQ is >= 2 ("poor").
|
|
||||||
1 is "bad": All of the above are not true or IAQ is 1 ("unhealthy") regardless of other values.
|
|
||||||
|
|
||||||
Note that IAQ levels hold a major sway over this level, and decreasing IAQ
|
|
||||||
scores will push the room score lower regardless of temperature or humidity.
|
|
||||||
It is best used together with the individual sensors to determine exactly
|
|
||||||
what is wrong with the room.
|
|
||||||
|
|
||||||
### Derived Text Sensors
|
|
||||||
|
|
||||||
#### VOC Level
|
|
||||||
|
|
||||||
This reports the VOC level alone, based on the scale under IAQ Index, in textual form ("Great, "Good", etc.).
|
|
||||||
|
|
||||||
#### CO2 Level
|
|
||||||
|
|
||||||
This reports the eCO2 level alone, based on the scale under IAQ Index, in textual form ("Great, Good", etc.).
|
|
||||||
|
|
||||||
#### IAQ Classification
|
|
||||||
|
|
||||||
This reports the IAQ Index in textual form ("Great", "Good", etc.).
|
|
||||||
|
|
||||||
#### Room Health
|
|
||||||
|
|
||||||
This reports the Room Health Score in textual form ("Optimal", "Fair", "Poor", "Bad").
|
|
||||||
|
179
supersensor.yaml
179
supersensor.yaml
@ -546,38 +546,65 @@ sensor:
|
|||||||
cpu_frequency:
|
cpu_frequency:
|
||||||
name: "CPU Frequency"
|
name: "CPU Frequency"
|
||||||
|
|
||||||
- platform: sgp30
|
- platform: sgp4x
|
||||||
eco2:
|
voc:
|
||||||
name: "SGP30 eCO2"
|
name: "SGP41 VOC Index"
|
||||||
id: sgp30_eco2
|
id: sgp41_voc_index
|
||||||
accuracy_decimals: 1
|
accuracy_decimals: 0
|
||||||
icon: mdi:molecule-co2
|
icon: mdi:waves-arrow-up
|
||||||
|
filters:
|
||||||
|
- 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:waves-arrow-up
|
||||||
filters:
|
filters:
|
||||||
- sliding_window_moving_average:
|
- sliding_window_moving_average:
|
||||||
window_size: 20
|
window_size: 12
|
||||||
send_every: 1
|
send_every: 3
|
||||||
tvoc:
|
|
||||||
name: "SGP30 TVOC"
|
|
||||||
id: sgp30_tvoc
|
|
||||||
accuracy_decimals: 1
|
|
||||||
icon: mdi:molecule
|
|
||||||
filters:
|
|
||||||
- sliding_window_moving_average:
|
|
||||||
window_size: 20
|
|
||||||
send_every: 1
|
|
||||||
eco2_baseline:
|
|
||||||
name: "SGP30 Baseline eCO2"
|
|
||||||
id: sgp30_baseline_ec02
|
|
||||||
icon: mdi:molecule-co2
|
|
||||||
tvoc_baseline:
|
|
||||||
name: "SGP30 Baseline TVOC"
|
|
||||||
id: sgp30_baseline_tvoc
|
|
||||||
icon: mdi:molecule
|
|
||||||
compensation:
|
compensation:
|
||||||
temperature_source: sht45_temperature
|
temperature_source: sht45_temperature
|
||||||
humidity_source: sht45_humidity
|
humidity_source: sht45_humidity
|
||||||
store_baseline: yes
|
store_baseline: true
|
||||||
update_interval: 15s
|
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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
- platform: sht4x
|
- platform: sht4x
|
||||||
temperature:
|
temperature:
|
||||||
@ -622,51 +649,6 @@ sensor:
|
|||||||
return (b * alpha) / (a - alpha);
|
return (b * alpha) / (a - alpha);
|
||||||
update_interval: 15s
|
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
|
|
||||||
}
|
|
||||||
update_interval: 15s
|
|
||||||
|
|
||||||
- platform: tsl2591
|
- platform: tsl2591
|
||||||
address: 0x29
|
address: 0x29
|
||||||
update_interval: 1s
|
update_interval: 1s
|
||||||
@ -795,59 +777,6 @@ text_sensor:
|
|||||||
mac_address:
|
mac_address:
|
||||||
name: "LD2410C MAC Address"
|
name: "LD2410C 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: ld2410
|
|
||||||
restart:
|
restart:
|
||||||
name: "LD2410C Restart"
|
name: "LD2410C Restart"
|
||||||
icon: mdi:power-cycle
|
icon: mdi:power-cycle
|
||||||
|
Reference in New Issue
Block a user