Fabriquer un capteur de CO2 Zigbee avec ESP32-H2 et MH-Z19 pour Home Assistant
Guide complet pour construire un capteur de CO2 Zigbee avec un ESP32-H2 Super Mini, un capteur MH-Z19 et une LED RGB. Firmware Arduino, intégration Zigbee2MQTT et carte Lovelace pour Home Assistant.
Pourquoi ce projet ?
Le MH-Z19 est l’un des capteurs NDIR de CO₂ les plus populaires grâce à son excellent rapport qualité/prix. L’objectif est de réaliser un capteur CO₂ entièrement local, fiable et intégré nativement à Home Assistant avec une consommation réduite.
Le projet repose sur un ESP32-H2 Super Mini utilisant la pile Zigbee native d’Espressif. En complément, une LED RGB diffuse à cathode commune fournit une indication visuelle immédiate de la qualité de l’air.
Pourquoi l’ESP32-H2 ?
- Zigbee natif ✅
- Faible consommation ✅
- Intégration Zigbee2MQTT ✅
- Disponibilité des GPIO ✅
- Coût réduit ✅
Le matériel nécessaire
Budget total : environ 22€ chez AliExpress.
| Composant | Photo | Rôle | Prix approx. | Lien |
|---|---|---|---|---|
| ESP32-H2 Super Mini | ![]() | Microcontrôleur principal | 5€ | AliExpress |
| MH-Z19 | ![]() | Capteur CO₂ NDIR (infrarouge) | 12€ | AliExpress |
| LED RGB diffuse cathode commune | ![]() | Indicateur visuel qualité d’air | 0.50€ | AliExpress |
| Résistances 330Ω (×3) | ![]() | Limitation courant LED RGB | 0.30€ | AliExpress |
| Carte PCB prototype 4×6 | ![]() | Support de soudure | 2€ | AliExpress |
| Fils silicone 22AWG | Câblage interne | 3€ | AliExpress | |
| Boîte acrylique G8DF | ![]() | Boîtier de protection | 1€ | AliExpress |
| Total | ~22€ |
Principe de fonctionnement
Le MH-Z19 communique en UART à 9600 bauds. Toutes les 5 secondes, l’ESP32-H2 interroge le capteur, extrait la concentration de CO₂ et la température, puis publie les valeurs via deux endpoints Zigbee :
- Endpoint 1 : température
- Endpoint 2 : CO₂
La LED RGB est pilotée selon les seuils suivants :
| Concentration CO₂ | Couleur LED | Signification |
|---|---|---|
| ≤ 800 ppm | 🟢 Vert | Air de bonne qualité |
| 801–1200 ppm | 🟡 Jaune | Qualité moyenne / VMC petite vitesse |
| 1201–1500 ppm | 🔴 Rouge | Aération conseillée / VMC grande vitesse |
| > 1500 ppm | 🔴 Rouge clignotant | Concentration élevée — ventilation urgente |
Câblage
Voici le schéma de câblage détaillant le raccordement du MH-Z19 (TX/RX, alimentation) ainsi que celui de la LED RGB avec les trois résistances de limitation à l’ESP32-H2.
Connexions
| ESP32-H2 | MH-Z19 | LED RGB |
|---|---|---|
| GPIO10 (RX) | TX | — |
| GPIO11 (TX) | RX | — |
| 5V | VCC | — |
| GND | GND | Cathode commune |
| GPIO14 | — | Rouge (via 330Ω) |
| GPIO13 | — | Vert (via 330Ω) |
| GPIO12 | — | Bleu (via 330Ω) |
Réalisation du PCB
La carte PCB a été réduite à l’aide d’une lime (ou ponceuse rotative) pour s’intégrer dans la boîte acrylique. Deux variantes de montage sont possibles.
Réalisé avec un MH-Z19B — soudures apparentes sur le dessus.
Autre possibilité avec un MH-Z1911A — soudures invisibles depuis le dessus.
Firmware Zigbee pour l’ESP32-H2
Le firmware ci-dessous est un sketch Arduino IDE utilisant la bibliothèque Zigbee d’Espressif. Il configure deux endpoints Zigbee (température et CO₂), lit le MH-Z19 en UART, et pilote la LED RGB.
⚠️ Prérequis : Assurez-vous que la bibliothèque « esp32 by Espressif Systems » est installée dans Arduino IDE.
Configuration Arduino IDE
Avant de flasher, configurez les paramètres suivants dans le menu Outils :
- USB CDC On Boot :
Enabled - Core Debug Level :
None - Erase All Flash Before Sketch Upload :
Disabled - Flash Frequency :
64MHz - Flash Mode :
QIO - Flash Size :
4MB (32Mb) - JTAG Adapter :
Disabled - Partition Scheme :
Zigbee 4MB with spiffs - Upload Speed :
921600 - Zigbee Mode :
Zigbee ED (end device)
Code source
#ifndef ZIGBEE_MODE_ED
#error "Zigbee End Device mode not selected"
#endif
#include "Zigbee.h"
#include "esp_sleep.h"
/* ================= UART MH-Z19 ================= */
#define MHZ19_RX 10
#define MHZ19_TX 11
HardwareSerial mhz19(1);
/* ================= Zigbee Endpoints ================= */
#define TEMP_ENDPOINT 1
#define CO2_ENDPOINT 2
ZigbeeTempSensor zbTemp(TEMP_ENDPOINT);
ZigbeeCarbonDioxideSensor zbCO2(CO2_ENDPOINT);
/* ================= Variables globales ================= */
static bool buttonPressed = false;
uint16_t currentCO2 = 0;
/* ================= Bouton reset ================= */
uint8_t BUTTON = BOOT_PIN;
/* ================= LED RGB ================= */
#define LED_R 14
#define LED_G 13
#define LED_B 12
/* ================= MH-Z19 Functions ================= */
bool readMHZ19(uint16_t &co2, float &temp) {
uint8_t cmd[9] = {0xFF, 0x01, 0x86, 0, 0, 0, 0, 0, 0x79};
uint8_t resp[9];
while (mhz19.available()) {
mhz19.read();
}
delay(10);
mhz19.write(cmd, 9);
delay(100);
if (mhz19.available() >= 9) {
mhz19.readBytes(resp, 9);
if (resp[0] == 0xFF && resp[1] == 0x86) {
co2 = (resp[2] << 8) | resp[3];
temp = resp[4] - 40;
return true;
}
}
return false;
}
/* ================= LED CO2 ================= */
void setCO2Led(uint16_t co2) {
// Clignotement rouge > 1500 ppm
if (co2 > 1500) {
bool blink = (millis() / 500) % 2;
ledcWrite(LED_R, blink ? 255 : 0);
ledcWrite(LED_G, 0);
ledcWrite(LED_B, 0);
return;
}
if (co2 <= 800) {
// Vert
ledcWrite(LED_R, 0);
ledcWrite(LED_G, 255);
ledcWrite(LED_B, 0);
}
else if (co2 <= 1200) {
// Jaune (Rouge + Vert)
ledcWrite(LED_R, 255);
ledcWrite(LED_G, 30);
ledcWrite(LED_B, 0);
}
else {
// Rouge
ledcWrite(LED_R, 255);
ledcWrite(LED_G, 0);
ledcWrite(LED_B, 0);
}
}
/* ================= Gestion Bouton ================= */
void checkButtonPress() {
static uint32_t lastPressTime = 0;
bool currentState = digitalRead(BUTTON);
if (currentState == LOW && !buttonPressed) {
lastPressTime = millis();
buttonPressed = true;
Serial.println("[Bouton] Appuyé");
}
if (buttonPressed && currentState == LOW) {
if (millis() - lastPressTime > 3000) {
Serial.println("[Bouton] >>> FACTORY RESET ZIGBEE <<<");
Zigbee.factoryReset();
delay(300);
ESP.restart();
}
}
if (currentState == HIGH && buttonPressed) {
buttonPressed = false;
Serial.println("[Bouton] Relâché");
}
}
/* ================= SETUP ================= */
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("\n========== ESP32-H2 BOOT ==========");
pinMode(BUTTON, INPUT_PULLUP);
// UART MH-Z19
mhz19.begin(9600, SERIAL_8N1, MHZ19_RX, MHZ19_TX);
delay(100);
Serial.println("[MHZ19] UART initialisé");
/* ===== LED RGB PWM ===== */
ledcAttach(LED_R, 5000, 8);
ledcAttach(LED_G, 5000, 8);
ledcAttach(LED_B, 5000, 8);
ledcWrite(LED_R, 0);
ledcWrite(LED_G, 0);
ledcWrite(LED_B, 0);
Serial.println("[LED] RGB initialisée");
/* ===== Configuration des capteurs Zigbee ===== */
const char* manufacturer = "Espressif";
const char* modelId = "MH-Z19_Temp_CO2";
zbTemp.setManufacturerAndModel(manufacturer, modelId);
zbTemp.setMinMaxValue(-10, 60);
zbTemp.setTolerance(0.2);
zbCO2.setManufacturerAndModel(manufacturer, modelId);
zbCO2.setMinMaxValue(400, 5000);
zbCO2.setTolerance(50);
Zigbee.addEndpoint(&zbTemp);
Zigbee.addEndpoint(&zbCO2);
Serial.println("[Zigbee] Démarrage...");
if (!Zigbee.begin()) {
Serial.println("[ERREUR] Zigbee.begin() a échoué !");
ESP.restart();
}
uint32_t startTime = millis();
while (!Zigbee.connected() && millis() - startTime < 30000) {
delay(100);
Serial.print(".");
}
Serial.println();
zbTemp.setReporting(5, 60, 0.2);
zbCO2.setReporting(5, 60, 5);
Serial.println("========== Setup Complet ==========\n");
}
/* ================= LOOP ================= */
void loop() {
checkButtonPress();
static uint32_t lastMeasure = 0;
if (millis() - lastMeasure >= 5000) {
uint16_t co2;
float temp;
if (readMHZ19(co2, temp)) {
currentCO2 = co2;
zbCO2.setCarbonDioxide(co2);
zbTemp.setTemperature(temp);
Serial.printf("[MHZ19] CO2: %d ppm | Temp: %.1f°C\n", co2, temp);
} else {
Serial.println("[MHZ19] Erreur de lecture");
}
lastMeasure = millis();
}
// LED mise à jour en continu
setCO2Led(currentCO2);
delay(100);
}
Intégration de la LED RGB
La LED RGB utilise une cathode commune reliée à la masse. Les GPIO 12, 13 et 14 pilotent respectivement les canaux bleu, vert et rouge via PWM.
Calibration des couleurs
Lors des essais, il est apparu que le vert était beaucoup plus lumineux que
le rouge. Le mélange (255, 255, 0) produisait donc un jaune verdâtre. Une
calibration des intensités a permis d’obtenir un jaune visuellement plus
équilibré : (255, 30, 0) pour le jaune.
Si vous souhaitez réduire l’intensité lumineuse (pour une chambre par
exemple), remplacez les valeurs 255 par 64 et 30 par 8 dans la
fonction setCO2Led().
Intégration Home Assistant
Une fois l’ESP32 flashé et alimenté (par exemple avec un chargeur de téléphone via l’USB-C), il vous suffit d’activer l’appairage sous Zigbee2MQTT (ou ZHA) pour voir votre capteur apparaître.
Carte Lovelace
Voici un exemple de carte Lovelace pour afficher le CO₂ sous forme de jauge :
type: gauge
entity: sensor.0x1051dbfffe62ec61_co2_2
name: CO₂ intérieur
min: 400
max: 2000
severity:
green: 400
yellow: 800
red: 1200
Résultat final
Une fois tout en place, vous obtenez :
- Un capteur CO₂ Zigbee entièrement local, sans cloud
- Une LED RGB indicative pour connaître la qualité de l’air en un coup d’œil
- Une intégration native dans Home Assistant via Zigbee2MQTT
- Une consommation électrique réduite grâce au mode Zigbee End Device
- Un bouton de reset (appui long 3 secondes) pour ré-appairer le capteur
Le tout pour moins de 25€ de matériel et une après-midi de montage.
Aller plus loin
- Le MH-Z19 peut également être calibré manuellement en envoyant une commande UART spécifique (mode calibration à 400 ppm).
- Pour une utilisation en extérieur, remplacez la boîte acrylique par un boîtier étanche IP65.
- Il est possible d’ajouter un capteur de température/humidité I²C (type BME280) sur le même ESP32-H2 pour enrichir les données.
Vous avez réalisé ce projet ? Vous avez des questions ? Les discussions sont ouvertes ci-dessous.
et télécharger les fichiers sources





