diff --git a/README.md b/README.md index 96e1ede..bf156d9 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,14 @@ The SuperSensor's voice functionality can be completely disabled if voice support is not desired. This defeats most of the point of the SuperSensor, but can be done if desired. +### Gas Ceiling + +The AQ (air quality) calculation from the BME680 requires a "maximum"/ceiling +threshold for the gas resistance value in clean air after some operation +time. The value defaults to 200 kΩ to provide an initial baseline, but +should be calibrated manually after setup as each sensor is different. See +the section "Calibrating AQ" below for more details. + ### Light Threshold Control The SuperSensor features a "light presence" binary sensor based on the light @@ -170,3 +178,139 @@ For detect, no occupancy will ever fire. For clear, no states will clear occupancy; with any detect option, this means that occupancy will be detected only once and never clear, which is likely not useful. + +## Calibrating AQ + +The Supersensor uses the Bosch BME680 combination temperature, humidity, +pressure, and gas sensor to provide a wide range of useful information about +the environmental conditions the sensor is placed in. However, this sensor +can be tricky to work with. + +While it's normally recommended to use the Bosch BSEC library with this +sensor, in my ~6 month experience I found this library to be far more trouble +than it was worth. Specifically, it's IAQ measurement is nearly useless, with +a strong tendency to get stuck in an upward trend constantly "calibrating" +itself to higher and higher baselines, to the point where nonsensical values +were being read. After much research into this, I decided to abandon the +library in version 1.1 and went with a more custom solution. + +Instead of the BSEC, we use the stock BME680 ESPHome library, along with +some calculations by thstielow on GitHub in their [IAQ project](https://github.com/thstielow/raspi-bme680-iaq). +This provided some useful example code and formulae to calculate a useful +Air Quality (AQ) value instead of the useless Bosch value. + +However using this method requires some manual calibration of the sensor +after putting it together but before final use, in order to get a somewhat +accurate value out of the AQ component. If you don't care about the AQ value, +you can skip this, but it is recommended to take full advantage of the sensor. + +As a quick explainer, the code leverages a combination of the "Gas Resistance" +value provided by the sensor, along with an absolute humidity calculated from +the temperature and relative humidity of the sensor (included ESPHome sensor), +along with two values (one configurable, one hard-coded) and several formulae +to arrive at the resulting AQ value. For full details of the calculation, +see the repository linked above, which was re-implemented faithfully here. + +The first thing to note is that each BME680 sensor is wildly different in +terms of gas resistance values. In the same air, I had sensors reading values +that differed by nearly 200,000Ω, which necessitates a human-configurable +baseline value. Further, the IAQ project recommends determining a linear +slope value for this, but instead of trying to explain how to calculate this, +I just went with the default slope value of 0.03 for this first iteration. + +Thus, the main difficulty in getting a useful AQ score is finding the +"Gas Resistance Ceiling" value. This value is configurable in the +SuperSensor interface (Web or HomeAssistant), and should be calibrated as +follows during the initial setup of the supersensor. + +1. Find a known-clean room, for instance a well-ventilated, well-cleaned +room in your house or similar. It should have fresh air (no stray VOCs) but +also minimal drafts or outside exposure especially if there is a poor external +AQ level. This will be your calibration reference room. Ideally, this room +should be somewhere between 16C and 26C for optimal performance, so air +conditioning (or a nice spring/fall day) is best. + +2. Turn on the SuperSensor in this environment, and connect it to your +HomeAssistant instance; this will be critical for viewing historical graphs +during the following steps. + +3. Let the SuperSensor run to "burn in" the gas sensor for at least 3-6 hours, +or until the value for the Gas Resistance stabilizes. It is best to avoid much +movement or activity in the selected calibration room to avoid disrupting +the sensor during this time. It is also best to ensure that the ambient +temperature changes as little as possible during this time. + +4. Review the resulting graph of Gas Resistance over the burn-in period. You +can usually ignore the first hour or two as the sensor was burning in, and +focus instead on the last hour or so. + +5. Make note of the highest mean value reached by the sensor during this time. +This will be your baseline value for calibrating the Gas Resistance Ceiling. + +6. Round the value up to the nearest 1000. For example, if the maximum value +was 195732.1, round this to 196000.0. + +7. Find the difference in the temperature of the BME680 temperature sensor +from 20C, called ΔT below. I found this part by trial-and-error, so this is +not precise, but as an example if the calibration room is reporting 26C, your +ΔT value in the next step is 6. If your temperature was below 20C, use 0. + +8. Use one of the following formulae to come up with your offset value, which +depends on the maximum value range found in step 6. + + * `<100,000`: 200 * ΔT = 0-1200 + * `100,000-200,000`: 500 * ΔT = 0-3000 + * `>200,000`: 1000 * ΔT = 0-6000 + +Again this value is rough, and might not even really be needed, but helps +avoid weird issues with AQ values dropping suddenly later as temperature +and humidity changes. + +9. Add your offset value from step 8 to the rounded maximum from step 6. +For example, 196000.0 with a ΔT of 5C (25C ambient) yields 201000.0 + +10. Divide the result from 9 by 1000 to give a number from 1-500. This +is the value to enter as the "Gas Resistance Ceiling (kΩ)" for this +sensor. This value will be saved in the NV-RAM of the ESP32 and preserved +on reboots. + +At this point, you should have a value that results in the "BME680 AQ" +sensor reporting 100% AQ, i.e. clean air. You can now test to ensure +that the value will correctly drop as VOCs are added. + +1. Take a Sharpie permanent marker, Acetone nail polish remover, or some +other VOC that the BME680 gas sensor can detect, and place it near the +sensor. For example with a sharpie, remove the cap and place the tip +about 1-2cm from the sensor, or place a small capful of nail polish +remover about 3-5cm from the sensor. + +2. Wait about 30 seconds. + +3. You should see the AQ value drop precipitously, into the order of 50% +or lower, and ideally closer to 0-20%. If the value remains higher than +50% with this test, your calculated Gas Resistance Ceiling might be +too low, and should be increased in increments of 1000. + +4. Remove the VOC source (replace the cap, remove the capful of remover, +etc.) and wait about 30-60 minutes. + +5. You should see the AQ value and gas resistance return to their original +values. If it is significantly lower than before, even after waiting 60+ +minutes, restart the calculation from step 5 in the previous section +using this new value as the baseline. + +At this point, the sensor should be calibrated enough for day-to-day +casual home use, and will tell you if there is any significant +VOC contamination in the air by dropping the AQ value from 100% to some +lower value representing the approximate decrease in air quality. Since +the sensor also factors in the absolute humidity (and via that, the +ambient temperature) into the AQ calculation, high humidity will also +drop the value, as this too impacts the air quality. Hopefully this +is useful for your purposes. + +If you find that the AQ value still doesn't represent known reality, +you can also tweak the in-code value for `ph_slope` on line 522, as +it's possible your sensor differs significantly here. As mentioned +above this is still a work in progress to determine for myself, so +future versions may alter this or include calibration of this value +automatically, depending on how things go in my testing. diff --git a/supersensor.yaml b/supersensor.yaml index 7fd01cf..17420c3 100644 --- a/supersensor.yaml +++ b/supersensor.yaml @@ -1,10 +1,10 @@ --- ############################################################################### -# SuperSensor v1.x ESPHome configuration +# SuperSensor v1.0 ESPHome configuration ############################################################################### # -# Copyright (C) 2023-2025 Joshua M. Boniface +# Copyright (C) 2023 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 @@ -26,7 +26,7 @@ esphome: friendly_name: "Supersensor" project: name: joshuaboniface.supersensor - version: "1.3" + version: "1.1" on_boot: - priority: 600 then: @@ -55,21 +55,26 @@ dashboard_import: esp32: board: esp32dev -# framework: -# type: esp-idf -# version: 4.4.8 -# platform_version: 5.4.0 -# sdkconfig_options: -# CONFIG_ESP32_DEFAULT_CPU_FREQ_240: "y" -# CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ: "240" -# CONFIG_ESP32_DATA_CACHE_64KB: "y" -# CONFIG_ESP32_DATA_CACHE_LINE_64B: "y" -# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" -# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ: "240" -# CONFIG_ESP32S3_DATA_CACHE_64KB: "y" -# CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" + framework: + type: esp-idf + version: 4.4.8 + platform_version: 5.4.0 + sdkconfig_options: + CONFIG_ESP32_DEFAULT_CPU_FREQ_240: "y" + CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ: "240" + CONFIG_ESP32_DATA_CACHE_64KB: "y" + CONFIG_ESP32_DATA_CACHE_LINE_64B: "y" + CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" + CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ: "240" + CONFIG_ESP32S3_DATA_CACHE_64KB: "y" + CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" globals: + - id: gas_resistance_ceiling + type: int + restore_value: yes + initial_value: "200" + - id: pir_hold_time type: int restore_value: yes @@ -292,12 +297,12 @@ interval: # wake_word: !lambda return wake_word; # Include the Espressif Audio Development Framework for VAD support -#esp_adf: -#external_components: -# - source: github://pr#5230 -# components: -# - esp_adf -# refresh: 0s +esp_adf: +external_components: + - source: github://pr#5230 + components: + - esp_adf + refresh: 0s voice_assistant: microphone: mic @@ -449,13 +454,6 @@ ld2410: # g8_move_threshold: 80 # g8_still_threshold: 81 -bme68x_bsec2_i2c: - address: 0x77 - model: bme680 - operating_age: 28d - sample_rate: LP - supply_voltage: 3.3V - binary_sensor: - platform: template name: "SuperSensor Occupancy" @@ -507,25 +505,25 @@ binary_sensor: name: "LD2410C Still Target" sensor: - - platform: bme68x_bsec2 + - platform: bme680 + address: 0x77 + update_interval: 5s + iir_filter: 127x temperature: name: "BME680 Temperature" id: bme680_temperature + oversampling: 16x pressure: name: "BME680 Pressure" id: bme680_pressure + oversampling: 16x humidity: name: "BME680 Relative Humidity" id: bme680_humidity - iaq: - name: "BME680 IAQ" - id: bme680_iaq - co2_equivalent: - name: "BME680 CO2 Equivalent" - id: bme680_co2e - breath_voc_equivalent: - name: "BME680 Breath VOC Equivalent" - id: bme680_bco2e + oversampling: 16x + gas_resistance: + name: "BME680 Gas Resistance" + id: bme680_gas_resistance - platform: absolute_humidity name: "BME680 Absolute Humidity" @@ -533,6 +531,28 @@ sensor: humidity: bme680_humidity id: bme680_absolute_humidity + - platform: template + name: "BME680 AQ" + id: bme680_aq + icon: "mdi:gauge" + unit_of_measurement: "%" + accuracy_decimals: 0 + update_interval: 5s + # Calculation from https://github.com/thstielow/raspi-bme680-iaq + lambda: |- + float ph_slope = 0.03; + float comp_gas = id(bme680_gas_resistance).state * pow(2.718281, (ph_slope * id(bme680_absolute_humidity).state)); + float gas_ratio = pow((comp_gas / (id(gas_resistance_ceiling) * 1000)), 2); + if (gas_ratio > 1) { + gas_ratio = 1.0; + } + float air_quality = gas_ratio * 100; + int normalized_air_quality = (int)air_quality; + if (normalized_air_quality > 100) { + normalized_air_quality = 100; + } + return normalized_air_quality; + - platform: tsl2591 address: 0x29 update_interval: 1s @@ -618,36 +638,29 @@ sensor: entity_category: diagnostic text_sensor: - - platform: bme68x_bsec2 - iaq_accuracy: - name: "BME68x IAQ Accuracy" - - platform: template - name: "BME68x IAQ Classification" + name: "BME680 AQ Classification" + icon: "mdi:air-filter" + update_interval: 5s lambda: |- - if ( int(id(bme680_iaq).state) <= 50) { + int aq = int(id(bme680_aq).state); + if (aq >= 90) { return {"Excellent"}; } - else if (int(id(bme680_iaq).state) >= 51 && int(id(bme680_iaq).state) <= 100) { + else if (aq >= 80) { return {"Good"}; } - else if (int(id(bme680_iaq).state) >= 101 && int(id(bme680_iaq).state) <= 150) { - return {"Lightly polluted"}; + else if (aq >= 70) { + return {"Fair"}; } - else if (int(id(bme680_iaq).state) >= 151 && int(id(bme680_iaq).state) <= 200) { - return {"Moderately polluted"}; + else if (aq >= 60) { + return {"Moderate"}; } - else if (int(id(bme680_iaq).state) >= 201 && int(id(bme680_iaq).state) <= 250) { - return {"Heavily polluted"}; - } - else if (int(id(bme680_iaq).state) >= 251 && int(id(bme680_iaq).state) <= 350) { - return {"Severely polluted"}; - } - else if (int(id(bme680_iaq).state) >= 351) { - return {"Extremely polluted"}; + else if (aq >= 50) { + return {"Bad"}; } else { - return {"error"}; + return {"Terrible"}; } - platform: wifi_info @@ -723,6 +736,21 @@ switch: entity_category: diagnostic number: + - platform: template + name: "Gas Resistance Ceiling (kΩ)" + id: gas_resistance_ceiling_setter + min_value: 10 + max_value: 500 + step: 1 + entity_category: config + lambda: |- + return id(gas_resistance_ceiling); + set_action: + then: + - globals.set: + id: gas_resistance_ceiling + value: !lambda 'return int(x);' + # PIR Hold Time: # The number of seconds after motion detection for the PIR sensor to remain held on - platform: template