Getting a CO2 reading out of a Sensirion SCD41 takes about ten lines of ESPHome YAML. Keeping that reading honest over months is the harder part. The sensor corrects its own drift with an algorithm that makes assumptions about your room, and if the room doesn't meet them, the numbers slowly move away from reality without any error in the logs. Wiring and YAML take a few minutes. Calibration, temperature offset and pressure decide whether the value in Home Assistant can be trusted, so most of what follows is about them.

SCD40, SCD41 or SCD43

The three sensors share a package, the I²C address (0x62) and the ESPHome scd4x platform. According to Sensirion's datasheet they differ mainly in accuracy and specified range:

  • SCD40: specified from 400 to 2,000 ppm, accuracy ±(50 ppm + 5% of reading).
  • SCD41: specified from 400 to 5,000 ppm, ±(50 ppm + 2.5% of reading) up to 1,000 ppm and ±(50 ppm + 3%) up to 2,000 ppm. It also has a single shot mode.
  • SCD43: specified from 400 to 5,000 ppm at ±(30 ppm + 3% of reading), with single shot mode.

For a monitor that runs from mains power and measures continuously, the SCD40 is enough for most rooms. The SCD41 makes sense if you expect readings above 2,000 ppm, for example in a small bedroom with closed windows, or if you want single shot mode later. The examples below use an SCD41, and the configuration is the same for the other two.

Wiring and power

The sensor talks I²C, so it needs four wires: VDD, GND, SDA and SCL. The datasheet allows a supply between 2.4 and 5.5 V, but it is strict about quality. Ripple on the supply must stay under 30 mV, peak current at 3.3 V is typically 175 mA (205 mA maximum), and Sensirion recommends a separate LDO regulator for the sensor. If you use a breakout board, check its schematic for a regulator. A bare sensor hanging off the same 3.3 V rail as the ESP32 and its Wi-Fi radio is exactly the setup the datasheet warns about.

Sensirion asks for external pull-up resistors on SDA and SCL (the datasheet's example is 10 kΩ). ESPHome enables the ESP32's internal pull-ups by default, but its documentation still recommends external ones for longer wires or several devices on the bus. Check whether your breakout board already has them.

One handling rule from the ESPHome page: don't remove or damage the white membrane on top of the sensor.

The ESPHome configuration

i2c:
  sda: GPIO21
  scl: GPIO22
  scan: true

sensor:
  - platform: scd4x
    id: my_scd41
    measurement_mode: periodic
    update_interval: 60s
    temperature_offset: 4
    altitude_compensation: 220m
    co2:
      name: "CO2"
    temperature:
      name: "Temperature"
    humidity:
      name: "Humidity"

GPIO21 and GPIO22 are the usual default I²C pins on an ESP32. Use whatever your wiring needs. With scan: true, the log shows the addresses found on the bus at boot, and the sensor should appear at 0x62. If it doesn't, check the wiring before touching anything else.

In periodic mode the sensor measures every 5 seconds on its own, and ESPHome reads the latest result every update_interval (60 seconds by default). low_power_periodic measures every 30 seconds, so update_interval must be at least 30 seconds. The single shot modes exist only on the SCD41 and SCD43. Stay with periodic or low_power_periodic unless you have a reason not to, because the calibration algorithm described below assumes one of them, or single shot measurements every 5 minutes.

ESPHome gives the CO2 entity the carbon_dioxide device class, the ppm unit and the measurement state class, so Home Assistant keeps long-term statistics for it without any extra setup.

Automatic self-calibration and the weekly fresh-air assumption

Automatic self-calibration (ASC) is on by default, both in the sensor and in ESPHome (automatic_self_calibration: true). Sensirion describes it like this: the algorithm assumes that, while measuring, the sensor sees outdoor fresh air at about 400 ppm for at least 3 minutes once a week. It treats the lowest value seen in a period as that 400 ppm baseline and shifts its output accordingly. By default the first correction happens after an initial period of 44 hours of operation, and later ones after every standard period of 156 hours.

In a living room or office that gets aired every few days, this works. It fails quietly when the assumption is false. If a room never drops near outdoor levels for a whole week, ASC still treats the lowest value it saw as 400 ppm and pulls the readings down. The sensor then shows healthy air in a room that isn't. Sensirion also warns that exposure to concentrations below 400 ppm with ASC on can affect accuracy.

Two more details from Sensirion's operation guide matter for a home build:

  • The ASC state is saved to non-volatile memory periodically, and power cycling can delay the next corrections. A sensor on a smart plug that switches off every night is a poor fit.
  • The datasheet states that the specified long-term accuracy assumes ASC with default parameters and weekly exposure to 400 ppm air. Turning ASC off is allowed, but then Sensirion no longer guarantees the accuracy.

If the room is aired regularly, leave ASC on. If it isn't, turn ASC off and calibrate by hand.

Forced recalibration from Home Assistant

Forced recalibration (FRC) tells the sensor: "the air around you right now has this much CO2". ESPHome has the scd4x.perform_forced_calibration action for it, and a template button is a convenient way to call it from Home Assistant:

button:
  - platform: template
    name: "Calibrate CO2 to 426 ppm"
    entity_category: config
    on_press:
      - scd4x.perform_forced_calibration:
          value: 426
          id: my_scd41

The value 426 comes from the ESPHome documentation, which cites it as the global monthly mean CO2 concentration in March 2025 according to NOAA. Outdoor air in a city or next to a busy road can be higher, so if you have a trusted reference meter, use its reading instead.

Sensirion's procedure for a good FRC:

  1. Run the sensor in its normal measurement mode for at least 3 minutes in air with a stable, uniform CO2 concentration. In practice that means outdoors or at a wide-open window, away from people, because your own breath raises the reading.
  2. Keep the same supply voltage the sensor uses in normal operation, and set the altitude or pressure compensation before calibrating.
  3. Only then press the button. The ESPHome action stops the measurement, sends the command and starts measuring again.

Sensirion adds two limits. After the sensor is soldered or assembled, wait at least 5 days before an FRC. And don't run FRC more often than once every 4 hours, because each one writes to the sensor's EEPROM and shortens its life. Keep that in mind before you put the calibration button on a dashboard that anyone can tap.

If you turned ASC off, plan regular FRC sessions. Sensirion describes this as the replacement for ASC in places that never get fresh air, but gives no fixed interval for home use.

Temperature offset: why the default is 4 °C

The SCD41 has a temperature and humidity sensor inside, and the sensor heats itself while measuring. The datasheet sets the default temperature offset to 4 °C and says the right value depends on the measurement mode, nearby components that warm up, the ambient temperature and airflow. An ESP32 in the same small box adds heat of its own.

Finding the right value takes a reference thermometer and some patience. Leave the finished device in its enclosure, in its final mode, until the readings stop changing. Then use Sensirion's formula:

new offset = SCD41 temperature - reference temperature + current offset

For example, if the SCD41 shows 24.1 °C, the reference shows 22.3 °C and the offset is 4, the new offset is 5.8. The recommended range is 0 to 20 °C. The offset does not change the CO2 reading. It only corrects the temperature and humidity outputs, so if those don't interest you, you can leave the default alone.

Altitude and pressure

The SCD41 compensates its CO2 output for air pressure, and by default assumes sea level (0 m). ESPHome gives you three ways to correct that:

  • altitude_compensation: the height of the room above sea level, in metres. Set it once for a device that doesn't move.
  • ambient_pressure_compensation: a fixed pressure in mbar. If it is set, the altitude setting is ignored.
  • ambient_pressure_compensation_source: the ID of a local pressure sensor, for example a BME280 on the same I²C bus. The ESPHome documentation notes that the pressure sensor must report in hPa (the same number as mbar). This is the most accurate option, because weather changes the pressure too.

If you set a BME280 as the source, the example in the ESPHome documentation also sets temperature_offset: 0 for the SCD4x. Whatever offset you end up using, find it with the procedure above.

Turning readings into action in Home Assistant

A useful scale for interpreting the number comes from the German Environment Agency (Umweltbundesamt). Its indoor air committee rates CO2 below 1,000 ppm as harmless, 1,000 to 2,000 ppm as elevated, calling for more ventilation, and above 2,000 ppm as unacceptable.

An automation on the raw value fires again and again when CO2 hovers around 1,000 ppm. The Threshold helper avoids that. Create it in Settings > Devices & services > Helpers > Create helper > Threshold, pick the CO2 entity as input, set the upper limit to 1000 and the hysteresis to 100. The resulting binary sensor turns on above 1,100 ppm and off below 900 ppm.

Then trigger on that binary sensor, with a delay so that one person breathing near the sensor doesn't set it off:

triggers:
  - trigger: state
    entity_id: binary_sensor.living_room_co2_high
    to: "on"
    for: "00:05:00"
actions:
  - action: notify.mobile_app_my_phone
    data:
      message: "CO2 in the living room is above 1100 ppm. Time to open a window."

Replace the entity and the notify action with your own. The same trigger can switch on a bathroom fan or a ventilation unit instead of sending a message.

Checklist when the readings look wrong

  • Readings slowly drift down over weeks in a room that is rarely aired: ASC is assuming fresh air it never gets. Turn it off and use FRC.
  • CO2 jumps up when you stand next to it: expected. Move the sensor away from where people sit and breathe.
  • Temperature several degrees too high: set the temperature offset. It does not affect CO2.
  • Nothing on the I²C bus at boot: check SDA/SCL wiring, pull-ups and the power supply before blaming the configuration.
  • Values off after moving house or to a higher floor: update altitude_compensation, or use a pressure sensor.