yesterday, I have implemented a ESP32 with a BME680 to measure the air quality in our living room.
The sensor is providing data: temperature and humidity seem to be in an expected range, but the VOCs are just way off.
I do have to old Airthings Wave Plus devices: one in the basement, one in the second floor.
I can see the radon coming from the basement to the second floor. there is a correlation between values in the basement and in the second floor. So there is an airflow going upward through the house…
VOCs in the basement are very low, VOCs in the second floor jump up and down. (50 - 1100 ppb)
The newly BME680 is running for almost 12h and VOC values are way to high. If this was true, the sensor in the second floor (screenshot red) would be much higher.
Question: who has experience with this sensor? Is this expected behaviour? How long does the calibration period take? (BME680 Numeric IAQ Accuracy is switching between 2 and 3.)
# sources:
# https://esphome.io/components/sensor/airthings_ble
# https://esphome.io/components/sensor/bme680_bsec.html
# Variablen
substitutions:
device_name: "esphome-wohnzimmer2"
friendly_name: "Wohnzimmer2"
node_name: "esphome_wohnzimmer2"
device_description: "Sensor liest den Airthings Wave Plus Sensor im Keller via Bluetooth aus und liefert Daten über den BME680"
# ESPHome Core Configuration
esphome:
name: '${device_name}'
friendly_name: '${friendly_name}'
comment: '${device_description}'
# Loop, der nach erfolgreicher WLAN-Verbindung und Zeitabfrage ausgeführt wird
on_boot:
priority: -100 # Setze die Priorität niedriger als die WLAN-Verbindung (standardmäßig -10)
then:
- while:
condition:
not:
lambda: 'return id(homeassistant_time).now().is_valid();'
then:
- delay: 1s # Warte eine Sekunde, bevor erneut überprüft wird
- lambda: |-
// Sobald die Zeit verfügbar ist, berechne den Boot-Zeitpunkt
id(${node_name}_last_boot_time).publish_state(
id(homeassistant_time).now().timestamp - (int) id(${node_name}_uptime_seconds).state
);
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
level: DEBUG
# Airthings Wave Plus finden
esp32_ble_tracker:
# airthings_ble:
# Enable Home Assistant API
api:
encryption:
key: "j...x"
ota:
- platform: esphome
password: "e7....1c"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip:
# Set this to the IP of the ESP
static_ip: 192.168.30.26
# Set this to the IP address of the router. Often ends with .1
gateway: 192.168.30.1
# The subnet of the network. 255.255.255.0 works for most home networks.
subnet: 255.255.255.0
use_address: ${node_name}.internal
domain: .internal
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: '${device_name}'
password: "Bf...6"
captive_portal:
# Zeit von HomeAssistant abfragen -> für Last Boot nötig
time:
- platform: homeassistant
id: homeassistant_time
switch:
- platform: restart
name: "ESPHome '${friendly_name}' neu starten"
# Airthings Wave Plus einbinden
ble_client:
- mac_address: A4:DA:32:B9:5A:A5
id: awp_Keller
# i2c-config für BME680-Sensor
i2c:
sda: GPIO21
scl: GPIO22
scan: false
id: bus_a
bme680_bsec:
id: bme680_internal
address: 0x77
temperature_offset: 0
iaq_mode: static
supply_voltage: 3.3V
sample_rate: ulp
state_save_interval: 6h
sensor:
# Airthings Wave Plus Keller
- platform: airthings_wave_plus
name: '${friendly_name} Keller'
ble_client_id: awp_Keller
update_interval: 5min # default
battery_update_interval: 24h # default
temperature:
name: "Keller Temperatur"
id: awp_Keller_temp
radon:
name: "Keller Radon"
id: awp_Keller_Radon
radon_long_term:
name: "Keller Radon Long Term"
id: awp_Keller_Radon_long
pressure:
name: "Keller Luftdruck"
id: awp_Keller_pressure
humidity:
name: "Keller Feuchtigkeit"
id: awp_Keller_humidity
co2:
name: "Keller CO2"
id: awp_Keller_co2
tvoc:
name: "Keller VOC"
id: awp_Keller_voc
illuminance:
name: "Keller ambient light"
id: awp_Keller_light
battery_voltage:
name: "Keller Batterie Spannung"
id: awp_Keller_volt
# Airthings Wave Plus Keller: Batterieanzeige in %
- platform: copy
source_id: awp_Keller_volt
name: 'Keller Battery Level'
id: awp_Keller_battery
unit_of_measurement: "%"
device_class: battery
accuracy_decimals: 0
filters:
- calibrate_linear:
- 2.2 -> 0
- 3.1 -> 100
# Airthings Wave Plus Keller: Taupunkt berechnen
- platform: template
name: "Keller Taupunkt"
lambda: |-
return (243.5*(log(id(awp_Keller_humidity).state/100)+((17.67*id(awp_Keller_temp).state)/
(243.5+id(awp_Keller_temp).state)))/(17.67-log(id(awp_Keller_humidity).state/100)-
((17.67*id(awp_Keller_temp).state)/(243.5+id(awp_Keller_temp).state))));
device_class: temperature
state_class: measurement
unit_of_measurement: °C
update_interval: 5min
icon: 'mdi:thermometer-water'
# Example configuration entry for BME680
# - platform: bme680
# temperature:
# name: "BME680 Temperature"
# pressure:
# name: "BME680 Pressure"
# humidity:
# id: "humidity"
# name: "BME680 Humidity"
# gas_resistance:
# id: "gas_resistance"
# name: "BME680 Gas Resistance"
# i2c_id: bus_a
# address: 0x77
# update_interval: 60s
# - platform: template
# name: "BME680 Indoor Air Quality"
# id: iaq
# icon: "mdi:gauge"
# # calculation: comp_gas = log(R_gas[ohm]) + 0.04 log(Ohm)/%rh * hum[%rh]
# lambda: |-
# return log(id(gas_resistance).state) + 0.04 * id(humidity).state;
# state_class: "measurement"
- platform: bme680_bsec
# ID of the bme680_bsec component to use for the next sensors.
# Useful when working with multiple devices
bme680_bsec_id: bme680_internal
temperature:
# Temperature in °C
name: "BME680 Temperature"
filters:
- median:
window_size: 5
send_every: 1
send_first_at: 1
pressure:
# Pressure in hPa
name: "BME680 Pressure"
sample_rate: ulp
filters:
- median:
window_size: 5
send_every: 1
send_first_at: 1
accuracy_decimals: 0
humidity:
# Relative humidity %
name: "BME680 Humidity"
sample_rate: ulp
filters:
- median:
window_size: 5
send_every: 1
send_first_at: 1
accuracy_decimals: 0
gas_resistance:
# Gas resistance in Ω
name: "BME680 Gas Resistance"
filters:
- median:
window_size: 5
send_every: 1
send_first_at: 1
iaq:
# Indoor air quality value
name: "BME680 IAQ"
id: iaq
filters:
- median:
window_size: 5
send_every: 1
send_first_at: 1
iaq_accuracy:
# IAQ accuracy as a numeric value of 0, 1, 2, 3
name: "BME680 Numeric IAQ Accuracy"
co2_equivalent:
# CO2 equivalent estimate in ppm
name: "BME680 CO2 Equivalent"
filters:
- median:
window_size: 5
send_every: 1
send_first_at: 1
accuracy_decimals: 0
breath_voc_equivalent:
# Volatile organic compounds equivalent estimate in ppm
name: "BME680 Breath VOC Equivalent Source"
id: voc_source
internal: true
filters:
- median:
window_size: 5
send_every: 1
send_first_at: 1
- platform: template
name: "BME680 Breath VOC Equivalent"
id: BME680_voc_calc
lambda: |-
return ( (id(voc_source).state) * 1000) ;
device_class: volatile_organic_compounds_parts
state_class: measurement
unit_of_measurement: ppb
accuracy_decimals: 0
update_interval: 5min
icon: 'mdi:molecule'
# Last Boot Sensor definieren (Berechnung findet einmalig beim Boot statt)
- platform: uptime
id: ${node_name}_uptime_seconds
name: "Uptime Sekunden"
internal: true
- platform: template
name: "Last Boot"
id: ${node_name}_last_boot_time
device_class: timestamp
entity_category: diagnostic
accuracy_decimals: 0
update_interval: never # Kein regelmäßiges Update nötig
text_sensor:
# Air Quality-Angaben
- platform: template
name: "BME680 IAQ Classification"
icon: "mdi:checkbox-marked-circle-outline"
lambda: |-
if (int(id(iaq).state) <= 50) {
return {"Excellent"};
}
else if (int(id(iaq).state) <= 100) {
return {"Good"};
}
else if (int(id(iaq).state) <= 150) {
return {"Lightly polluted"};
}
else if (int(id(iaq).state) <= 200) {
return {"Moderately polluted"};
}
else if (int(id(iaq).state) <= 250) {
return {"Heavily polluted"};
}
else if (int(id(iaq).state) <= 350) {
return {"Severely polluted"};
}
else if (int(id(iaq).state) <= 500) {
return {"Extremely polluted"};
}
else {
return {"unknown"};
}
# Send IP Address of ESP-device
- platform: wifi_info
ip_address:
name: IP Address
- platform: bme680_bsec
iaq_accuracy:
# IAQ accuracy as a text value of Stabilizing, Uncertain, Calibrating, Calibrated
name: "BME680 IAQ Accuracy"
EDIT
I have put the sensor in a freshly printed case (PLA). I have read that PLA emits VOCs during printing, but I found no information about emiting VOCs afterwards as well?
EDIT2
AFAIK, there should be a correlation between gas resistance and VOC-values?
I have gone back to the documentation and have realised that ESPhome recommends to use this:
So I did.
I am still very disappointed by this sensor. VOC-values are still x-times to high - although not jumping around anymore - and temperature is 7°C too high.
The temperature offset is configurable, but the VOCs are not - AFAIK.
That’s a lot… Do you have it inside an enclosure?
It could mess up temperature compensation for other values as well.
You will never find two VOC-sensors that output same value. Also CO2 equivalent will never be like true NDIR sensor output, because it’s just an estimation based on VOCs.
Well, I am aware that sensors are not at all an exact thing - well, at least in that price segment. But I was expecting something comparable? My goal is to define a template, indicating when opening the windows would be a good idea. (logic based on air quality, radon, temperature, dew point (indoor vs. outdoor))
For temp, RH and pressure yes. For VOC no.
But for your use case it’s enough to detect relative values and that’s IAQ should give you.
But until you find out why temp is off so much, I wouldn’t trust that sensor.
Well, I assume, the temperature is too high, because the gas resistance sensor heats up to do his measurement. I could try to force the sensor to take values at different times, and average them with a filter.
Do you know, what happens, if I configure the temperature offset for the BME680? (There is a configuration variable on top level of the sensor.) Will this affect only temperature or all sensor value calculation, such as VOC and CO2?
As said VOC sensors will give you a relative value.
What you can do is try to make a translation table between your VOC sensors.
Place them side by side to each other. Run a couple of days and compare the graphs.
Then switch their places around and do it again.
Next put them on top of each other and do it again.
Then switch them around here too and do it again.
I found some posts indicating that the temperature offset will have an influence on the other values as well. The BSEC-library should do the calculation, but I haven’t had the time to properly test it.
Unfortunately, I was not able to find the source on the bosch website to support this.
Thank you for the proposal. Well, I decided to focus on the IAQ-values. That should - in theory - be sufficient for my use case.
But if I compare the IAQ-value from the BME680 to my AirthingsWave Plus, the BME680 indicates “moderately polluted” (IAQ of 160, just after closing the windows), where as the Airthings sensor provides an all green state. (Outside air quality here in Switzerland is usually very very good, so I tend to believe the Airthings… )
In the end, they both should tell me to open the windows at the same sate of indoor air quality.
I will do some more research and try to find a way to get rid off the temperature differences. Might even re-design my casing.
EDIT: is there a cheap and exact CO2-sensor? If VOC-sensors are just useless, exact CO2-values would be a feasible work around…
Not really, good NDIR sensors are expensive. But compared to VOC sensors Co2 equivalent even the cheapest one are way better.
Senseair S8, MH-Z19, SCD40 …
Not really, depends on price/availability. Also SCD in aliexpress cost 1/3 compared to other markets, so I expect those to be something else than (first grade) products form Sensirion.
I’m trying to say, there is not wrong or right. There is no formula based on math or physics to give right output. There are different VOC sensors measuring several (different) gases different way and there are different formulas to give some output. Take it like this: output 1 or 10 doesn’t matter. But 2 is twice as 1 and 20 is twice as 10.