Introduction
Simple DIY module for monitoring air quality using the ESP32 Mini with ESPHome firmware to integrate with Home Assistant. We’ll use the AHT21 and CCS811 sensors to measure Humidity, Temperature, Total Volatile Organic Compound (TVOC), and CO2 levels, and display the data on a 1.3-inch SH1106 OLED display.
All components are connected using an I2C interface through a DIY I2C rail. Additionally, the module includes an AUX button that can be set to toggle the display screen or cycle through display pages.

This project is budget-friendly, costing around $12 for the essential components, and offers a hands-on experience that combines electronics, coding, and 3D printing.
I designed this module specifically to monitor fumes from my 3D printer, ensuring a safer and healthier workspace. Whether you’re a seasoned maker or a beginner, this project is an exciting addition to your smart home setup.
Material List
3D PRINT PARTS
- Thingiverse.com: https://www.thingiverse.com/thing:6684495
- Printable.com: https://www.printables.com/model/932420-simple-esp32-aqi-module-enclosure-with-display
COMPONENTS FOR THIS PROJECT
- ESP32 Mini Module: Aliexpress | Shopee Thailand
- AHT21 Sensor (for humidity and temperature): Aliexpress | Shopee Thailand
- CCS811 Sensor (for TVOC and CO2): Aliexpress | Shopee Thailand
- 1.3-inch SH1106 OLED Display: Aliexpress | Shopee Thailand
- 6.5×14.5cm Strip Prototype Board: Aliexpress | Shopee Thailand
- JST-XH 2.54 connectors: Aliexpress | Shopee Thailand
- Dupont connector (Optional): Aliexpress | Shopee Thailand
- 1/4W Resistor: Aliexpress | Shopee Thailand
- 6x6x6 Push Button: Aliexpress | Shopee Thailand
- M3x10 Button Head Screws: Aliexpress | Shopee Thailand
- M2 Self Tapping Screws: Aliexpress | Shopee Thailand
- M2 Plastic Washer: Aliexpress | Shopee Thailand
Key Features of AHT21 and CCS811 Sensors
AHT21 Sensor
The AHT21 is an inexpensive and highly accurate temperature and humidity sensor. It offers a relative humidity accuracy of ±2% and a temperature accuracy of ±0.3 °C.
These specifications make it a reliable choice for indoor air quality monitoring, ensuring that the readings you get for humidity and temperature are precise and consistent.
With its compact size and I2C interface, the AHT21 is easy to integrate into your DIY projects. The I2C interface greatly reduces wiring clutter by enabling multiple sensors on the same bus using just a pair of data wires from the MCU.
This making it an excellent option for various applications, including environmental monitoring and smart home setups.

AHT21 Technical Parameter
- Supply voltage: DC 2.2 – 5.5V
- Measuring range (humidity): 0 ~ 100% RH
- Measuring range (temperature): -40 ~ + 120 ℃
- Humidity accuracy: ± 2 % RH ( 25 ℃ )
- Temperature accuracy: ± 0.3 ℃
- Resolution: temperature: 0.01℃ Humidity: 0.024%RH
CCS811 Sensor
The CCS811 is a versatile air quality sensor capable of measuring equivalent calculated carbon dioxide (eCO2) and Total Volatile Organic Compound (TVOC) concentrations.
It can detect eCO2 concentrations within a range of 400 to 8192 parts per million (ppm) and TVOC concentrations within a range of 0 to 1187 parts per billion (ppb).

According to the datasheet, the CCS811 can detect a variety of organic compounds, including:
- Alcohols
- Aldehydes
- Ketones
- Organic Acids
- Amines
- Aliphatic Hydrocarbons
- Aromatic Hydrocarbons
These capabilities make the CCS811 an essential component for monitoring indoor air quality, as it can provide insights into the presence of harmful pollutants and help ensure a healthier living environment.
Please note, this sensor, like all VOC/gas sensors, has variability and to get precise measurements you will need to calibrate it against known sources. That said, for general environmental sensors, it will give you a good idea of trends and comparisons.

DIY I2C Hub
To simplify wiring and split the I2C interface. I made a DIY I2C hub using a single-side strip prototype board.
Although it’s slightly more fragile than the green prototype board I usually use, this board is easier to cut and significantly reduces the time required to bridge connections between each connector.
The prototype board was cut to dimensions of 5 rows by 14 columns. Then you can soldered components according to the following diagram.
The pull-up resistors (3.3-10KΩ) were place in parallel from both SDA and SCL line to VCC line. You also need 4 pins JST-XH connectors for the input from ESP32 and output to AHT21 and the OLED display. While CCS811 sensor will need 5 pin connector because it need extra WAK pin that need to connect to GND.

For more information on the I2C bus and the importance of pull-up resistors, you can refer to this useful resource: How Many Devices Can You Connect to the I2C Bus?

Momentary Button
I’ve also use strip prototype board for mounting the momentary switch for the AUX button. I’ve cut the strip board to 5 rows by 6 columns.
I use 2mm drill bit to enlarge the corner holes for mounting it to the enclosure with M2x6 self-tap screws.

I’ve connect ground wire to pin 1 of 6×6 push button and the wire from GPIO27 to pin 3 to trigger the binary sensor.
The 3D Print Enclosure
I print the enclosure with eSun PLA+, it’s easy to print and suitable for indoor use.
The enclosure consists of two parts: a cover and a bottom section. The display is mounted to the cover using M2x8 self-tapping screws.
You may also need plastic washer if the display’s mounting holes are too large for the M2 screw, as the mount hole size can vary depending on the supplier.
I’ve used M3x10 ball head screws to secure the cover to the bottom part. You may need to tap the hole with an M3 tap for easier to screwing in.
The ESP32, sensors, and I2C hub are mounted in the provided slots on the bottom part and secured in place using hot melt glue or 3M VHB tape.

ESPHome YAML
The YAML configuration for AHT21 and CCS811 sensor:
According to data sheet recoomendation, the CCS811 need to burn-in for 48 hours when you first receive it, and then 20 minutes in the desired mode every time the sensor is in use.
This process helps stabilize the sensor’s sensitivity levels, which can fluctuate during early use.
To calibrate the sensor and get baseline: value to obtain more accurate readings, please refer to the “Calibrating Baseline” section in the ESPHome documentation.
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: aht10
variant: AHT20
temperature:
name: Temperature
id: aht_temp
humidity:
name: Humidity
id: aht_humid
update_interval: 60s
- platform: ccs811
id: WS_css811
eco2:
name: "eCO2"
id: eco2
tvoc:
name: "TVOC"
id: tvoc
address: 0x5A
baseline: 0x90BB #Replace with value from your calibration
temperature: aht_temp
humidity: aht_humid
update_interval: 60s
- platform: internal_temperature
name: "ESP32 Temperature"
id: int_temp
The YAML configuration for AUX button:
I’ve use a binary_sensor component for an AUX button that wire to GPIO27. Then use on_click: to control both short and long presses of the button.
Depending on the press duration, it triggers actions through “template switch” or “template button”. These actions toggle the display on or off and enable page navigation within the module.
This AUX button can easily customise, you can edit its action or add additional template buttons or template switches to perform different functions.
Please note that all template buttons and switches are exposed to Home Assistant, allowing control directly from the dashboard.
Binary Sensor: Monitors GPIO27 as an input and handles two types of button presses:
- Short press (50ms to 350ms): Triggers
button.pressaction forpage_button. - Long press (3s to 10s): Toggles the
display_switch.
binary_sensor:
- platform: gpio
name: "Switch 1"
id: aqi_switch1
internal: True
pin:
number: GPIO27
inverted: true
mode:
input: true
pullup: true
on_click:
- min_length: 50ms
max_length: 350ms
then:
- button.press: page_button
- min_length: 3s
max_length: 10s
then:
- switch.toggle: display_switch
Template Button: Perform some action turn on the display and switch display to next page.
button:
- platform: template
name: "Page Button"
id: page_button
on_press:
- lambda: id(aqi_display).turn_on();
- lambda: id(aqi_display).set_contrast(1.0);
- display.page.show_next: aqi_display
- component.update: aqi_display
Template Switch: Represents the state of the display and use Checks the current contrast level of the display (lambda:) to determine the switch state.aqi_display
switch:
- platform: template
name: "Display Switch"
id: display_switch
lambda: |-
if (id(aqi_display).get_contrast() > 0) {
return true;
} else {
return false;
}
turn_on_action:
- lambda: id(aqi_display).turn_on();
- lambda: id(aqi_display).set_contrast(1.0);
- delay: 30s
- lambda: id(aqi_display).set_contrast(0.5);
turn_off_action:
- lambda: id(aqi_display).set_contrast(0.0);
- lambda: id(aqi_display).turn_off();
The YAML configuration for SH1106 Display:
font:
#Opensans
- file: 'fonts/OpenSans-Regular.ttf'
id: opensans_reg_12
size: 12
#Opensans - Bold
- file: 'fonts/OpenSans-Bold.ttf'
id: opensans_bold_10
size: 10
- file: 'fonts/OpenSans-Bold.ttf'
id: opensans_bold_14
size: 14
#Opensans - Condense Bold
- file: 'fonts/OpenSans-Bold.ttf'
id: opensans_CondBold_24
size: 24
- file: 'fonts/OpenSans-CondBold.ttf'
id: opensans_CondBold_36
size: 36
display:
- platform: ssd1306_i2c
model: "SH1106 128x64"
id: aqi_display
address: 0x3C
contrast: 1.0
pages:
- id: page1
lambda: |-
// Print TVOC
it.printf(32, 0, id(opensans_bold_14), TextAlign::TOP_CENTER, "TVoc:");
if (id(tvoc).has_state()) {
it.printf(32, 18, id(opensans_CondBold_24), TextAlign::TOP_CENTER, "%.0f", id(tvoc).state);
} else {
it.printf(32, 18, id(opensans_CondBold_24), TextAlign::TOP_CENTER, "N/A");
}
it.printf(32, 48, id(opensans_bold_10), TextAlign::TOP_CENTER, "ppb");
// Print eco2
it.printf(96, 0, id(opensans_bold_14), TextAlign::TOP_CENTER, "eCO2:");
if (id(eco2).has_state()) {
it.printf(96, 18, id(opensans_CondBold_24), TextAlign::TOP_CENTER, "%.0f", id(eco2).state);
} else {
it.printf(96, 18, id(opensans_CondBold_24), TextAlign::TOP_CENTER, "N/A");
}
it.printf(96, 48, id(opensans_bold_10), TextAlign::TOP_CENTER, "ppm");
- id: page2
lambda: |-
// Print Temp
it.printf(0, 0, id(opensans_bold_10), TextAlign::TOP_LEFT, "Temperature:");
if (id(aht_temp).has_state()) {
it.printf(127, 0, id(opensans_bold_14), TextAlign::TOP_RIGHT, "%.1f°C", id(aht_temp).state);
}
// Print Humid
it.printf(0, 24, id(opensans_bold_10), TextAlign::TOP_LEFT, "Humidity:");
if (id(aht_humid).has_state()) {
it.printf(127, 24, id(opensans_bold_14), TextAlign::TOP_RIGHT, "%.1f%%", id(aht_humid).state);
}
// Print CPU Temp
it.printf(0, 48, id(opensans_bold_10), TextAlign::TOP_LEFT, "CPU Temp:");
if (id(int_temp).has_state()) {
it.printf(127, 48, id(opensans_bold_14), TextAlign::TOP_RIGHT, "%.1f°C", id(int_temp).state);
}
Conclusion
With just a simple ESP32 development module, a couple of sensors, and a little time, we can make a compact AQI module to monitor real-time environmental data and personalised control in your smart home setup.
Unlike off-the-shelf solutions, this DIY module offers several advantages. Not only is it significantly cheaper than commercial equivalents, but it also provides greater control and customisation options. The ESPHome firmware is very powerful and easy to use. It allows you to tailor the module to your specific needs.





1 Response
[…] For more information about wiring diagram and component list please refer to main project page: Simple DIY AQI Module: Measure TVOC, CO2, Humidity, and Temperature […]