Skip to content

Instantly share code, notes, and snippets.

@tekk
Last active September 29, 2025 19:10
Show Gist options
  • Select an option

  • Save tekk/15433a60dfa6cdd2e40d93eef1d05539 to your computer and use it in GitHub Desktop.

Select an option

Save tekk/15433a60dfa6cdd2e40d93eef1d05539 to your computer and use it in GitHub Desktop.

SCD40 CO2 Sensor with ESP8266 D1 mini and ESPHome

Connect a Sensirion SCD40 CO2 sensor to an ESP8266 D1 mini and run it with ESPHome. This allows you to measure CO2, temperature, and humidity in your room and integrate the data into Home Assistant, Prometheus, or MQTT.

Wiring

SCD40 ESP8266
VCC 3V3
GND G
SCL D1 (GPIO5)
SDA D2 (GPIO4)

The white sticker on the SCD40 module is a protective membrane and must remain in place.

ESPHome configuration

Use the provided scd40.yaml configuration with ESPHome. It includes support for CO2, temperature, humidity, captive portal, OTA upgrades, MQTT, Prometheus metrics, and manual baseline calibration.

Flash the firmware the first time via USB with esphome run scd40.yaml. Subsequent updates can be done wirelessly using OTA.

Home Assistant integration

Enable the ESPHome API integration in Home Assistant. Entities for CO2, temperature, humidity, Wi-Fi RSSI, and uptime will be available automatically. MQTT integration is also possible if desired.

Prometheus integration

ESPHome can expose Prometheus metrics at the /metrics endpoint when the web_server and prometheus components are enabled. Use a Prometheus server running on another machine to scrape these metrics and visualize them with Grafana.

Calibration

The configuration includes a manual calibration feature. Ventilate the room with fresh outdoor air, then trigger calibration with the exposed button entity. The sensor will adjust its baseline to the specified target ppm value.

esphome:
name: scd40-co2
platform: ESP8266
board: d1_mini
wifi:
ssid: "WiFi-CO2"
password: "ChangeMe1234"
fast_connect: true
ap:
ssid: "SCD40 Fallback"
password: "Fallback1234"
captive_portal:
# Local password-protected web UI (and JSON endpoints)
web_server:
port: 80
auth:
username: admin
password: "WebAdmin12345678" # Rather change me
# Native API (HA autodiscovery) with encryption
api:
encryption:
key: "P1ab2KNr9bC4KYx8wb5dzm94l2e3bc3" # Change me obviously
# OTA upgrades
ota:
password: "OTA_CO2_12345678" # Change me as well
logger:
# MQTT alongside API (disable HA MQTT discovery to avoid duplicate entities)
mqtt:
# IP of HA, change to yours
broker: 192.168.1.2
username: "esphome" # Your HA
password: "esphome123" # Creds
discovery: false
topic_prefix: scd40-co2
# Prometheus /metrics endpoint (requires web_server)
prometheus:
include_internal: true
# I2C for SCD40
i2c:
sda: D2
scl: D1
scan: true
id: bus_a
# Built-in D1 mini LED as status (GPIO2/D4, active LOW)
status_led:
pin:
number: D4
inverted: true
time:
- platform: sntp
id: sntp_time
# --- Sensors ---
sensor:
- platform: scd4x
id: scd40
automatic_self_calibration: true
# Adjust if you measure systematic temp bias, in deg. Celsius
temperature_offset: 0.0
co2:
name: "CO2"
id: scd40_co2
# Force events every update if you want time-series with constant steps
# force_update: true
temperature:
name: "Temperature"
id: scd40_temp
humidity:
name: "Humidity"
id: scd40_rh
update_interval: 60s
- platform: wifi_signal
name: "WiFi RSSI"
update_interval: 30s
- platform: uptime
name: "Uptime Seconds"
id: uptime_s
# Human-readable uptime + device/network info
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
ssid:
name: "WiFi SSID"
bssid:
name: "WiFi BSSID"
mac_address:
name: "MAC Address"
- platform: template
name: "Uptime"
icon: mdi:timer-outline
lambda: |-
uint32_t t = (uint32_t) id(uptime_s).state;
uint32_t sec = t % 60;
uint32_t min = (t / 60) % 60;
uint32_t hr = (t / 3600) % 24;
uint32_t day = (t / 86400);
char buff[32];
sprintf(buff, "%ud %02u:%02u:%02u", day, hr, min, sec);
return {buff};
# --- Manual Baseline Calibration Workflow ---
# 1) Choose target ppm (default to current global monthly mean).
number:
- platform: template
id: calib_target_ppm
name: "Calibration Target (ppm)"
optimistic: true
min_value: 350
max_value: 1200
step: 1
initial_value: 426
# 2) Press the button when room air is stable & well-ventilated.
button:
- platform: template
name: "Calibrate CO2 Now (fresh air in the room)"
icon: mdi:tune-variant
on_press:
- scd4x.perform_forced_calibration:
id: scd40
value: !lambda "return (int) id(calib_target_ppm).state;"
- platform: template
name: "Factory Reset SCD40"
icon: mdi:cog-refresh
on_press:
- scd4x.factory_reset: scd40
# Maintenance helpers
switch:
- platform: restart
name: "Restart Device"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment