If you want actual CO₂ readings in Home Assistant, use an optical CO₂ sensor such as Sensirion SCD40 or SCD41—not an estimated “eCO₂” value from a VOC sensor. Both SCD4x variants measure carbon dioxide using photoacoustic optical sensing and provide temperature and humidity outputs over I²C. An ESP32 running ESPHome can publish all three measurements without custom firmware.
The SCD40 covers ordinary room ventilation monitoring; the SCD41 extends the specified CO₂ range and adds an on-demand single-shot mode for projects with a constrained power budget. This guide explains what the specification differences mean, how to wire either sensor safely, and how to avoid calibration and enclosure mistakes that can make a perfectly good sensor look wrong.
SCD40 vs SCD41: The practical differences
| Feature | SCD40 | SCD41 |
|---|---|---|
| Sensing principle | Photoacoustic optical CO₂ | Photoacoustic optical CO₂ |
| Specified CO₂ measurement range | 400–2,000 ppm | 400–5,000 ppm |
| Published CO₂ output range | 0–40,000 ppm; not the accuracy range | 0–40,000 ppm; not the accuracy range |
| Accuracy in specified range | ±(50 ppm + 5% of reading), 400–2,000 ppm | Improved and range-dependent; see Sensirion’s latest specification |
| Normal periodic measurement | Every 5 s | Every 5 s |
| Low-power periodic mode | Every 30 s | Every 30 s |
| Single-shot CO₂ measurement | Not available | Available; approximately 5 s per measurement |
| Temperature and humidity | Included | Included |
| I²C address | 0x62 | 0x62 |
| ESPHome component | scd4x | scd4x |
Sensirion’s current product pages list the SCD40 accuracy as ±(50 ppm + 5% of reading) from 400–2,000 ppm. The SCD41 provides an extended specified range to 5,000 ppm and improved accuracy specifications across subranges. Do not confuse the 0–40,000 ppm output range with a promise of accuracy up to 40,000 ppm.
For a normal bedroom, office or living-room monitor, either device provides useful measured CO₂ data. The SCD41 is particularly relevant if you expect higher concentrations, need its tighter specified accuracy, or want single-shot operation.
What “real CO₂” means here
The SCD4x series uses Sensirion’s photoacoustic optical technology. Its measurement is based on CO₂ absorption of infrared light, rather than inferring CO₂ from a different gas. That distinction matters when comparing it with a VOC sensor: cooking, perfume or cleaning products can cause a VOC-derived eCO₂ estimate to change even when actual CO₂ does not change proportionally.
A VOC sensor and an optical CO₂ sensor are complementary, not interchangeable. Keep VOC/IAQ trends for odour and chemical-source detection; use an SCD40/SCD41 when ventilation decisions depend on measured CO₂ concentration. The SCD4x is not a carbon-monoxide (CO) detector or a substitute for certified life-safety equipment.
Parts and wiring for a classic ESP32 DevKit
Use an SCD40/SCD41 breakout with an explicit 3.3 V logic specification, a classic ESP32 board, suitable cables and a ventilated enclosure. Many breakout boards add pull-up resistors, regulators and level shifting; the bare sensor’s supply specifications do not tell you what every third-party module does.
| Sensor breakout | Classic ESP32 DevKit | Notes |
|---|---|---|
| VCC / VIN | 3V3 | Use 3.3 V if the breakout supports it; check seller schematic |
| GND | GND | Common ground required |
| SDA | GPIO21 | Default I²C data example |
| SCL | GPIO22 | Default I²C clock example |
The bare SCD4x sensor supports a broad supply range, but ESP32 GPIOs are not 5 V tolerant. A breakout powered at 5 V can pull the I²C lines to 5 V unless it provides appropriate level translation. If the board labels are unclear, check the pull-up rail with a meter or use a documented 3.3 V-compatible module. Also account for the sensor’s brief measurement-current peaks when selecting the regulator; a weak 3.3 V rail can cause communication errors.
The I²C address is fixed at 0x62
SCD40 / SCD41 I²C address: 0x62
Unlike a BME280, the normal SCD4x interface does not provide a jumper for choosing a second address. Two SCD40/SCD41 devices on the same bus will respond simultaneously unless you put them on separate buses or isolate them with an I²C multiplexer.
For two identical-address devices, a TCA9548A can create separate logical I²C channels. An ESP32 with spare I²C-capable pins can alternatively run multiple configured buses. Always identify which physical board is being read before calibrating it.
Minimal ESPHome configuration
ESPHome’s current scd4x platform supports both devices; you do not select an SCD40 or SCD41 model in the basic YAML. The component detects the connected sensor. The following is a complete starting node for a standard ESP32 DevKit:
esphome:
name: bedroom-co2
friendly_name: Bedroom CO2
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
i2c:
sda: GPIO21
scl: GPIO22
scan: true
sensor:
- platform: scd4x
address: 0x62
measurement_mode: periodic
automatic_self_calibration: true
update_interval: 60s
co2:
name: "Bedroom CO2"
id: bedroom_co2
temperature:
name: "Bedroom SCD4x Temperature"
humidity:
name: "Bedroom SCD4x Humidity"
On startup, ESPHome’s I²C scan should find device 0x62. Home Assistant then receives separate CO₂, temperature and humidity entities through the Native API. Leave the measurement and update settings conservative until you have a stable baseline and a good mounting position.
Why 60-second update_interval is not a low-power mode
The two timing settings control different things. In measurement_mode: periodic, the SCD4x performs a measurement about every five seconds. ESPHome’s update_interval: 60s controls how often the component checks and publishes data; it does not tell the sensor to sleep for the other 55 seconds.
If power matters, set the actual sensor measurement mode rather than merely reducing the number of Home Assistant updates.
Low-power periodic mode: works on SCD40 and SCD41
sensor:
- platform: scd4x
measurement_mode: low_power_periodic
update_interval: 60s
co2:
name: "Room CO2"
temperature:
name: "Room Temperature"
humidity:
name: "Room Humidity"
Here the sensor measures about every 30 seconds instead of every five seconds. ESPHome requires update_interval to be at least 30 seconds for this mode. Both SCD40 and SCD41 support it. For a mains-powered indoor monitor, normal periodic mode is usually a straightforward choice; low-power periodic trades measurement frequency for reduced sensor energy use.
SCD41 single-shot mode for on-demand measurements
SCD41—not SCD40—supports single_shot CO₂ measurement. Each measurement takes approximately five seconds. This makes sense for a device that deliberately measures less frequently; it does not automatically make the entire ESP32 node low power, because Wi-Fi and the development board’s regulator can still dominate total consumption.
sensor:
- platform: scd4x
measurement_mode: single_shot
update_interval: 5min
co2:
name: "Battery Node CO2"
temperature:
name: "Battery Node Temperature"
humidity:
name: "Battery Node Humidity"
The manufacturer’s automatic self-calibration behaviour is optimised for a particular sampling history; ESPHome notes that SCD41 single-shot ASC is optimised around a five-minute measurement interval. Power-cycling the sensor between every reading may prevent the necessary calibration history from accumulating. Deep sleep must also be designed around the sensor’s conversion time, network startup and actual storage behaviour; do not assume this YAML alone makes a reliable sleeping battery node.
Never use single_shot_rht_only for CO₂ monitoring
ESPHome also exposes the SCD41 mode single_shot_rht_only. This completes temperature/humidity acquisition quickly, but the CO₂ value is reported as 0 ppm. It is only appropriate when CO₂ is not part of the requested measurement. Do not build a ventilation automation around the 0 ppm result.
Automatic self-calibration: when it works
Automatic self-calibration (ASC) is enabled by default in ESPHome. It uses measurement history and an assumption about the lowest background concentration that the sensor regularly encounters. Sensirion’s standard ASC configuration assumes periodic exposure to outdoor-fresh-air-like CO₂ levels—not that an occupied sealed room permanently sits at one fixed concentration.
Sensirion’s software documentation describes the default expectation as exposure to air around a 400 ppm baseline at least once per week of accumulated operation, with adequate continuous measurement history. The actual outdoor baseline varies by location and time. If a room is continually occupied, a laboratory has controlled elevated CO₂, or the device is repeatedly powered down, ASC can gradually learn an inappropriate baseline.
A practical rule: keep ASC enabled for an ordinary home room that is ventilated to outdoor air regularly; consider disabling it for controlled atmospheres or spaces that never reach a representative fresh-air baseline, then use a verified calibration procedure.
sensor:
- platform: scd4x
automatic_self_calibration: false
co2:
name: "Controlled Room CO2"
Disabling ASC is not a magic accuracy upgrade. It means you own the long-term calibration process. Make that decision based on how the sensor is actually used, not because a forum post recommends always turning ASC off.
Forced recalibration: do not guess the reference concentration
ESPHome offers a scd4x.perform_forced_calibration action. Use it only after the device has operated for more than three minutes in its normal measurement mode, in air with a known, homogeneous and stable reference CO₂ concentration. A reading from a trusted reference instrument is preferable to assuming every outdoor location is precisely 400 ppm.
# Add this action to an existing ESPHome automation
# after stabilising in known reference air.
on_...:
then:
- scd4x.perform_forced_calibration:
id: room_scd4x
value: 425 # Example only: replace with measured reference ppm
The fragment above illustrates the documented action syntax; it is not standalone deployable YAML because on_... must be replaced with a real ESPHome trigger and room_scd4x must be the configured component ID. Do not expose an unprotected “calibrate to 400” button to casual Home Assistant dashboard use. One incorrect recalibration can make the entire trend misleading.
Temperature offset: the ESPHome default surprises people
ESPHome documents temperature_offset with a default of 4°C. This is compensation for the sensor’s temperature rise relative to surrounding air, influenced by measurement mode, airflow and neighbouring electronics. It is not an instruction to subtract an arbitrary extra four degrees from the Home Assistant temperature entity.
Check your installed device against a reference thermometer after thermal equilibrium. If the reading is consistently too warm because the ESP32 and regulator heat the shared enclosure, first improve physical layout. Then adjust the SCD4x component’s temperature offset based on a stable measurement rather than stacking contradictory filters.
sensor:
- platform: scd4x
temperature_offset: 4.0 # ESPHome default; tune only after a real comparison
co2:
name: "Room CO2"
temperature:
name: "SCD4x Temperature"
humidity:
name: "SCD4x Humidity"
The SCD4x’s temperature and RH outputs are valuable for internal compensation and convenient trends, but Sensirion’s published temperature/humidity accuracy for this optical sensor family is more modest than a dedicated precision environmental sensor. If tight room-temperature or humidity accuracy is your primary requirement, use a separate, appropriately placed sensor such as an AHT20 or SHT4x.
Place the sensor in real room air—not in an ESP32 hot box
The sensing module needs airflow representative of the room. Put it in a ventilated enclosure away from direct sunlight, radiators, exhausts, kitchen vapour jets and the ESP32 regulator. Position it where people breathe normally but not directly in someone’s exhaled plume; a person talking into the housing can produce a large short-lived spike.
- Keep air openings unobstructed; do not remove or damage the sensor’s protective white membrane.
- Provide separation from the ESP32 Wi-Fi module and power regulator, especially if using its temperature/RH outputs.
- Avoid placing it at floor level, beside an open window, or directly in a ventilation supply jet when you want a representative room value.
- Allow the housing to equalise after moving the sensor between rooms before treating the readings as a baseline.
- Do not permanently seal it in a “waterproof” enclosure: the sensor must exchange gas with surrounding air.
Ambient pressure and altitude compensation
CO₂ optical measurement depends on gas pressure. ESPHome supports a static ambient_pressure_compensation value in hPa/mbar, or altitude_compensation in metres. If ambient pressure compensation is supplied, ESPHome ignores the altitude setting. Use actual local station pressure, not a weather service’s sea-level-adjusted pressure, for pressure compensation.
If a BME280 is installed on the same device, ESPHome can pass its local pressure reading to the SCD4x. This is especially useful if the installation has a meaningful elevation difference or varying local pressure.
i2c:
sda: GPIO21
scl: GPIO22
scan: true
sensor:
- platform: bme280_i2c
address: 0x76
pressure:
id: local_pressure
name: "Local Pressure"
temperature:
name: "BME280 Temperature"
humidity:
name: "BME280 Humidity"
- platform: scd4x
ambient_pressure_compensation_source: local_pressure
co2:
name: "Pressure-Compensated CO2"
This is an optional precision improvement. For a first bench test, make the SCD4x work and verify its I²C address before adding an extra sensor or attempting advanced compensation.
Home Assistant: use trends and hysteresis rather than one instant spike
When the Native API is connected, Home Assistant can graph CO₂ alongside temperature and humidity. For an initial dashboard, show current CO₂ in ppm, a 24-hour history chart and the device’s uptime/Wi-Fi RSSI. Look for the shape of the trend when occupants enter, windows open or ventilation starts rather than treating every five-second fluctuation as a separate event.
A simple example alert can use a sustained high threshold. The following is an illustrative comfort/ventilation automation, not a legal exposure limit or occupational safety system:
alias: Bedroom - Sustained high CO2 reminder
triggers:
- trigger: numeric_state
entity_id: sensor.bedroom_co2
above: 1200
for: "00:10:00"
actions:
- action: persistent_notification.create
data:
title: "Bedroom ventilation"
message: "CO2 has stayed above 1200 ppm for 10 minutes. Check ventilation."
mode: single
Add a lower reset threshold or use Home Assistant’s Threshold helper with hysteresis before switching a fan repeatedly. A stable high CO₂ trend is a reason to investigate ventilation; CO₂ alone does not measure particulate matter, carbon monoxide, total VOCs or every component of indoor air quality.
SCD40/SCD41 compared with BME680 “eCO₂”
| Question | SCD40 / SCD41 | BME680 + IAQ algorithm |
|---|---|---|
| Does it directly measure CO₂? | Yes, optical/photoacoustic method | No: calculated equivalent/estimate |
| Can cleaning chemicals distort the main indication? | Not in the same VOC-proxy way | VOC-sensitive output can respond strongly |
| Reports temperature/humidity? | Yes | Yes |
| Measures VOC/gas-resistance trend? | Not its main output | Yes |
| Useful for ventilation tracking? | Measured CO₂ indicator | Complementary VOC/odour indicator |
Combining the two sensors gives a fuller picture than substituting one for the other. A room can have low CO₂ but elevated VOCs after painting, or elevated CO₂ from occupants without a strong VOC event.
Common faults and first checks
| Symptom | First diagnostic check |
|---|---|
| No device at 0x62 | Power, SDA/SCL swap, breakout pull-ups and I²C pins |
| I²C errors while ESP32 transmits | Regulator capacity, supply peaks, cable length and ground |
| SCD41 shows 0 ppm | Ensure you did not select single_shot_rht_only |
| CO₂ drifts down unrealistically in sealed room | Revisit ASC assumptions; verify with known reference air |
| CO₂ always near high values | Placement, actual ventilation and sensor calibration history |
| Temperature reads warm / RH reads low | Sensor/ESP32 self-heating and enclosure airflow |
| Publishing every 60s still draws power | Use measurement_mode, not just update_interval |
| Two sensors on one bus fail | Both use fixed 0x62; use separate bus or multiplexer |
| HA unavailable while serial sensor values continue | Check Wi-Fi / Native API before blaming SCD4x |
Which sensor should you choose?
Choose SCD40 for a mains-powered room monitor where you mainly need CO₂ trends in its 400–2,000 ppm specified range and a low-complexity ESPHome setup. Choose SCD41 when you specifically want the longer 400–5,000 ppm specified range, its improved accuracy specification, or the single-shot mode for an appropriately engineered lower-power node.
Both use the same ESPHome scd4x component, the same nominal I²C address and the same ordinary four-wire wiring. The largest gains in a home build often come from sensible enclosure design, representative placement, a stable supply and correct calibration assumptions—not from publishing the same reading to Home Assistant more frequently.
Related esp32.co.uk/ guides
- AHT20 with ESP32 and ESPHome — an inexpensive dedicated temperature/humidity option for a second reference sensor.
Official documentation
- ESPHome SCD4x Component — configuration, measurement modes, offsets and calibration actions.
- Sensirion SCD40 — current product specifications and downloads.
- Sensirion SCD41 — specified range and single-shot capability.
- Sensirion SCD4x driver documentation — automatic self-calibration requirements.
- Home Assistant Threshold helper — avoid output chatter with hysteresis.