Practical case: CO2 ventilation relay with Raspberry Pi

Practical case: co2 ventilation relay controller with — hero

Objective and use case

What you’ll build: A Raspberry Pi controller that reads CO2 concentration from an MH-Z19B UART sensor and switches a relay to start or stop a ventilation fan based on configurable ppm thresholds. In a typical setup, the Pi polls the sensor every 1–2 seconds, drives the relay with sub-100 ms GPIO response time, and uses negligible compute resources (<5% CPU, 0% GPU).

Why it matters / Use cases

  • Automate air-quality control in a home office, classroom, workshop, or meeting room when CO2 rises above levels such as 1000–1200 ppm.
  • Reduce unnecessary fan runtime by turning ventilation off when CO2 falls below a lower threshold, for example 800 ppm, to avoid relay chatter and save energy.
  • Learn practical Raspberry Pi interfacing with UART sensors, GPIO relay control, threshold logic, and basic environmental automation.
  • Create a foundation for extensions such as logging ppm trends, adding alerts, or publishing data to a dashboard.

Expected outcome

  • Stable CO2 readings from the MH-Z19B over UART, typically updated every 1–2 seconds.
  • Reliable relay switching when CO2 crosses defined thresholds, with GPIO actuation latency typically under 100 ms after a new reading is processed.
  • A simple hysteresis-based control loop, for example relay ON above 1000 ppm and OFF below 800 ppm, to prevent rapid toggling.
  • A lightweight always-on service suitable for Raspberry Pi, usually consuming minimal system load (<5% CPU, 0% GPU, no FPS requirement).

Audience: Raspberry Pi beginners, makers, and students building environmental automation; Level: Basic

Architecture/flow: MH-Z19B CO2 sensor → Raspberry Pi UART read → threshold/hysteresis logic → GPIO output → isolated relay module → ventilation fan or low-voltage test load

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. The validator checked the code blocks, article structure, copy/paste-safe commands and consistency with the supported device catalog.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 3 tables and 7 code blocks detected before publication.
  • Checked code: 1 Python/py_compile, 6 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published material, but it does not replace physical testing on your exact hardware, wiring and runtime environment.

Educational safety note

Safety note

This project switches mains-powered ventilation equipment through a relay. Use a relay module with proper isolation, follow local electrical rules, and keep mains wiring physically separated from low-voltage Raspberry Pi wiring. If you are not qualified to work with AC mains, use the relay only to drive a low-voltage test load such as an LED module or ask a qualified electrician to connect the fan.


title: Raspberry Pi Practical Case: CO2-Based Ventilation Relay Controller
slug: practical-case-raspberry-pi
level: Basic
device: Raspberry Pi
objective: co2-ventilation-relay-controller


CO2 Ventilation Relay Controller with Raspberry Pi

This practical case shows how to build a CO2-based ventilation relay controller on a Raspberry Pi. The Raspberry Pi reads indoor CO2 concentration from an MH-Z19B UART sensor and switches a relay to enable or disable a ventilation fan when the measured value crosses defined thresholds.

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

MH-Z19B CO2 sensor

Raspberry Pi UART read

threshold/hysteresis logic

GPIO output

isolated relay module

ventilation fan or low-voltage test load

Conceptual signal and responsibility flow between device blocks.

Objective

Implement a ventilation controller that:

  • Reads CO2 ppm
  • Turns the ventilation relay ON when CO2 is high
  • Turns the ventilation relay OFF when CO2 drops
  • Uses hysteresis to avoid relay chatter
  • Runs continuously as a simple Raspberry Pi service script

Hardware

  • Raspberry Pi with 40-pin GPIO header
  • MicroSD card with Raspberry Pi OS
  • MH-Z19B CO2 sensor
  • 5 V relay module, 1 channel, GPIO-compatible
  • External 5 V supply for the relay module if required by the module current
  • Jumper wires
  • Optional: low-voltage lamp or LED load for safe bench testing

Wiring

Pin Mapping

FunctionDevice PinRaspberry Pi PinNotes
5 V for sensorMH-Z19B VinPin 2 (5V)Sensor requires 5 V
Ground for sensorMH-Z19B GNDPin 6 (GND)Common ground
Sensor TXMH-Z19B TXPin 10 (GPIO15 / RXD)Pi receives sensor data
Sensor RXMH-Z19B RXPin 8 (GPIO14 / TXD)Optional for command support
Relay inputRelay INPin 11 (GPIO17)Active-low example in software
Relay VCCRelay VCC5 V supplyCheck module rating
Relay GNDRelay GNDPi GNDCommon ground required

Notes

  • Many relay boards are active-low. The software below assumes that driving GPIO17 LOW energizes the relay.
  • The MH-Z19B UART interface is 3.3 V logic safe on TX to Pi RX on common breakout boards. Verify your module before wiring.
  • On Raspberry Pi OS, the serial console must be disabled so /dev/serial0 is free for the sensor.

Enable UART on Raspberry Pi

Run:

sudo raspi-config nonint do_serial_hw 0
sudo raspi-config nonint do_serial_cons 1
sudo reboot

After reboot, verify the UART device exists:

ls -l /dev/serial0

Install Dependencies

sudo apt update
sudo apt install -y python3 python3-rpi.gpio

Controller Program

Save this file as co2_ventilation_controller.py.

#!/usr/bin/env python3
import time
import serial
import RPi.GPIO as GPIO

UART_DEVICE = "/dev/serial0"
UART_BAUD = 9600

RELAY_GPIO = 17
RELAY_ACTIVE_LOW = True

CO2_ON_THRESHOLD = 1000   # ppm: turn ventilation ON at or above this level
CO2_OFF_THRESHOLD = 800   # ppm: turn ventilation OFF at or below this level

POLL_INTERVAL_SECONDS = 5.0


def relay_write(enabled: bool) -> None:
    if RELAY_ACTIVE_LOW:
        GPIO.output(RELAY_GPIO, GPIO.LOW if enabled else GPIO.HIGH)
    else:
        GPIO.output(RELAY_GPIO, GPIO.HIGH if enabled else GPIO.LOW)


def mhz19_checksum(packet: bytes) -> int:
    checksum = 0xFF - (sum(packet[1:8]) % 256) + 1
    return checksum & 0xFF


def read_co2_ppm(ser: serial.Serial) -> int:
    command = bytes([0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79])
    ser.reset_input_buffer()
    ser.write(command)
    response = ser.read(9)

    if len(response) != 9:
        raise RuntimeError(f"Incomplete response: got {len(response)} bytes")

    if response[0] != 0xFF or response[1] != 0x86:
        raise RuntimeError(f"Unexpected response header: {response.hex()}")

    if response[8] != mhz19_checksum(response):
        raise RuntimeError(f"Checksum error: {response.hex()}")

    co2_ppm = response[2] * 256 + response[3]
    return co2_ppm


def main() -> None:
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(RELAY_GPIO, GPIO.OUT)
    relay_enabled = False
    relay_write(relay_enabled)

    print("Starting CO2 ventilation relay controller")
    print(f"ON threshold : {CO2_ON_THRESHOLD} ppm")
    print(f"OFF threshold: {CO2_OFF_THRESHOLD} ppm")

    try:
        with serial.Serial(UART_DEVICE, UART_BAUD, timeout=1.0) as ser:
            while True:
                try:
                    co2_ppm = read_co2_ppm(ser)

                    if not relay_enabled and co2_ppm >= CO2_ON_THRESHOLD:
                        relay_enabled = True
                        relay_write(relay_enabled)
                    elif relay_enabled and co2_ppm <= CO2_OFF_THRESHOLD:
                        relay_enabled = False
                        relay_write(relay_enabled)

                    state = "ON" if relay_enabled else "OFF"
                    print(f"CO2={co2_ppm} ppm, ventilation relay={state}")
                except RuntimeError as exc:
                    print(f"Read error: {exc}")

                time.sleep(POLL_INTERVAL_SECONDS)
    finally:
        relay_write(False)
        GPIO.cleanup()


if __name__ == "__main__":
    main()

Run the Controller

python3 co2_ventilation_controller.py

Expected console output:

  • CO2=650 ppm, ventilation relay=OFF
  • CO2=1100 ppm, ventilation relay=ON

Optional: Start Automatically at Boot

Create a systemd unit file:

sudo tee /etc/systemd/system/co2-ventilation.service >/dev/null <<'EOF'
[Unit]
Description=CO2 Ventilation Relay Controller
After=network.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi
ExecStart=/usr/bin/python3 /home/pi/co2_ventilation_controller.py
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF

Enable and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now co2-ventilation.service
sudo systemctl status co2-ventilation.service --no-pager

How the Ventilation Control Works

The Raspberry Pi implements a simple CO2 ventilation controller with hysteresis:

CO2 valueRelay actionVentilation state
>= 1000 ppmEnergize relayVentilation ON
801 to 999 ppmKeep previous stateStable, no chatter
<= 800 ppmRelease relayVentilation OFF

This avoids rapid switching near a single threshold.

Validation Method

To validate the CO2 ventilation relay controller behavior:

  1. Power the sensor and relay module.
  2. Run the Python script.
  3. Observe live output in the terminal.
  4. Expose the sensor to a higher CO2 level, for example by breathing near the sensor briefly in a ventilated test setup.
  5. Confirm:
  6. The printed CO2 value rises above 1000 ppm
  7. The ventilation relay changes to ON
  8. Move the sensor back to fresh air and wait for the reading to fall.
  9. Confirm:
  10. The printed CO2 value drops to 800 ppm or lower
  11. The ventilation relay changes to OFF

Expected Evidence

  • Terminal logs showing CO2 values crossing both thresholds
  • Audible relay clicks or LED indicator change on the relay board
  • Optional multimeter continuity check on relay contacts
  • Optional airflow or fan activation when the ventilation output is connected

Troubleshooting

SymptomLikely causeFix
Incomplete responseUART not enabled or wrong serial deviceRe-enable UART and check /dev/serial0
Constant checksum errorsWiring issue or baud mismatchRecheck TX/RX and use 9600 baud
Relay always ONModule is active-lowKeep RELAY_ACTIVE_LOW = True
Relay never switchesWrong GPIO pin or no common groundVerify GPIO17 wiring and GND
CO2 reading does not changeSensor warm-up or bad placementLet sensor warm up for several minutes

Result

You now have a working Raspberry Pi CO2 ventilation relay controller that reads a CO2 sensor and switches a relay to control ventilation automatically.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What sensor is used to measure CO2 concentration in the project?




Question 2: How often does the Raspberry Pi typically poll the CO2 sensor?




Question 3: What device is switched by the relay in this setup?




Question 4: What is a typical upper threshold mentioned for turning ventilation on?




Question 5: Why is a lower CO2 threshold such as 800 ppm used?




Question 6: What control method is used to avoid rapid toggling of the relay?




Question 7: What is the typical GPIO response time in this project?




Question 8: Which Raspberry Pi interface is used to read data from the MH-Z19B sensor?




Question 9: What kind of system load does the project usually consume?




Question 10: Which of the following is mentioned as a possible extension of the project?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top