SCD41 + ESP32 DevKitC v4 with ESPHome and MQTT (High-Precision CO₂ Monitor)

Building a reliable CO₂ monitor is one of the best “bang-for-buck” ESPHome projects. The Sensirion SCD41 is a true CO₂ sensor (photoacoustic sensing / PAS), not a “calculated eCO₂” VOC estimate, so it’s ideal for proper IAQ monitoring.

Using MQTT keeps your sensor data decoupled from Home Assistant: the same measurements can feed Node-RED, InfluxDB, Grafana, custom dashboards, and HA at the same time.

This guide covers wiring, ESPHome flashing, MQTT topics, calibration, and Home Assistant setup.

Part 1: Hardware and wiring

Components

  • ESP32 DevKitC v4 (classic ESP32 / ESP-WROOM-32 DevKitC)
  • Sensirion SCD41 breakout/module (Adafruit, SparkFun, or generic)
  • Jumper wires

Wiring (I²C)

The DevKitC v4 commonly uses:

  • SDA = GPIO21
  • SCL = GPIO22
SCD41 pinESP32 DevKitC v4 pinNotes
VIN / VCC3V3Use 3.3V for safest logic compatibility
GNDGNDGround
SDAGPIO21I²C data
SCLGPIO22I²C clock

Important: Even if your breakout claims 5V tolerant, the SCD41 chip is fundamentally a 3.3V device. 3V3 is the safe default.

Part 2: ESPHome YAML (MQTT-first, stable, copy/paste)

This config:

  • Connects to Wi-Fi
  • Publishes to MQTT under a clean topic_prefix
  • Reads CO₂, temperature, and humidity from SCD41 via I²C
  • Keeps OTA enabled for easy updates
  • Avoids “random reboots because no API client connected” (common gotcha)

Note: The “reboot if no API client connects” setting belongs to the ESPHome native API, not MQTT. If you’re MQTT-only, either disable api: or set api.reboot_timeout: 0s.

living-room-co2.yaml

esphome:
  name: living-room-air-monitor
  friendly_name: "Living Room CO2"

esp32:
  board: esp32dev        # DevKitC v4 (classic ESP32). NOT ESP32-C3.
  framework:
    type: esp-idf

# Logging (leave enabled for troubleshooting)
logger:
  # Optional: if you get boot noise or want UART silent:
  # baud_rate: 0

wifi:
  ssid: "YOUR_WIFI_SSID"
  password: "YOUR_WIFI_PASSWORD"
  ap:
    ssid: "CO2-Monitor-Fallback"
    password: "change_this_password"

captive_portal:

ota:
  - platform: esphome

# If you don't need ESPHome native API, you can remove this block entirely.
# If you keep it, disable reboot timeout so MQTT-only use won't restart the device.
api:
  reboot_timeout: 0s

mqtt:
  broker: 192.168.1.100
  username: "mqtt_user"
  password: "mqtt_password"

  # All topics will be under:
  # home/living-room-air-monitor/...
  topic_prefix: "home/living-room-air-monitor"

  # Optional (recommended): let HA discover entities automatically via MQTT Discovery.
  discovery: true

i2c:
  sda: 21
  scl: 22
  scan: true
  id: bus_a

sensor:
  - platform: scd4x
    i2c_id: bus_a
    update_interval: 60s

    co2:
      name: "CO2 Level"
      accuracy_decimals: 0
      unit_of_measurement: "ppm"

    temperature:
      name: "Temperature"
      accuracy_decimals: 1
      # If the sensor sits close to the ESP32, you may see heat bias.
      # Start with -1.0 to -3.0 and tune.
      filters:
        - offset: -2.0

    humidity:
      name: "Humidity"
      accuracy_decimals: 1
      unit_of_measurement: "%"

Why 60 seconds update interval?

The SCD41 can update faster, but for room monitoring:

  • 60 seconds keeps MQTT traffic reasonable
  • reduces self-heating effects
  • gives stable trends for ventilation decisions

Part 3: MQTT topics

With topic_prefix: "home/living-room-air-monitor" ESPHome will publish sensor states under that tree. The exact entity/object IDs can differ depending on naming, so don’t guess—verify once.

Verify with MQTT Explorer

  1. Connect MQTT Explorer to your broker.
  2. Expand:
    home/living-room-air-monitor
  3. You should see payloads updating every 60 seconds.

Typical paths look like:

  • .../sensor/co2_level/state
  • .../sensor/temperature/state
  • .../sensor/humidity/state

(Exact topic names depend on how ESPHome slugifies the sensor names; MQTT Explorer removes all doubt.)

Part 4: Calibration (this matters)

Automatic Self Calibration (ASC)

By default, many CO₂ sensors use a background calibration assumption:

  • Over time, the sensor expects that the lowest CO₂ level it sees occasionally is “fresh air” (~400 ppm).

ASC works well if:

  • the room regularly gets fresh air (open windows / doors) at least weekly

ASC can drift if:

  • the sensor never sees near-outdoor air (e.g., sealed room, always occupied)

If you cannot guarantee fresh air exposure

Disable ASC (if you choose to do so) and do manual calibration occasionally. ESPHome supports forced calibration actions for SCD4x; a common method is calibrating near outdoor fresh air (not next to traffic).

If you want a “press a button in HA to calibrate” approach, it can be done safely, but it should be treated carefully (calibrate in the wrong conditions and you’ll bake the error in).

Practical rule: Don’t add “forced calibration” buttons unless you’re sure you’ll only press them under correct conditions.

Part 5: Home Assistant integration (two ways)

Option A (recommended): MQTT Discovery (no YAML sensors)

If you set:

mqtt:
  discovery: true

Then Home Assistant will usually create entities automatically (Settings → Devices & services → MQTT).

Option B: Manual MQTT sensors

If you prefer explicit mqtt: sensors in configuration.yaml, do this after confirming the topics in MQTT Explorer:

mqtt:
  sensor:
    - name: "Living Room CO2"
      state_topic: "home/living-room-air-monitor/sensor/co2_level/state"
      unit_of_measurement: "ppm"
      device_class: carbon_dioxide
      state_class: measurement

    - name: "Living Room Temperature"
      state_topic: "home/living-room-air-monitor/sensor/temperature/state"
      unit_of_measurement: "°C"
      device_class: temperature
      state_class: measurement

    - name: "Living Room Humidity"
      state_topic: "home/living-room-air-monitor/sensor/humidity/state"
      unit_of_measurement: "%"
      device_class: humidity
      state_class: measurement

Part 6: Troubleshooting (fast)

I²C scan shows no device

  • SDA/SCL swapped is the #1 mistake
  • Confirm power is 3V3
  • Check ESPHome logs: you should see I²C scan results (SCD4x usually at 0x62)

CO₂ jumps wildly

  • Give it a few hours after first power-up
  • Avoid direct airflow blasts (AC vent blowing on it)
  • Avoid mounting it above warm electronics

Temperature reads too high

  • Your ESP32 heat is biasing it
  • Increase negative offset (e.g., -1.5, -2.0, -2.5) until it matches a trusted reference

MQTT entities don’t appear in HA

  • Verify broker IP/credentials
  • Check MQTT Explorer to ensure data is publishing
  • If using discovery: confirm HA MQTT integration is connected to the same broker

Nice next step: IAQ dashboard in HA

Once CO₂ is in Home Assistant, add a simple gauge card and a ventilation automation (e.g., alert if CO₂ > 1200 ppm for 10 minutes).

Share your love

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *