Scrugo
Terminé DIY Difficulté : ●●●○○

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.

⏱ 4h 💰 22€ 📅
Arduino IDE Zigbee ESP32 Home Assistant Soudure
Fabriquer un capteur de CO2 Zigbee avec ESP32-H2 et MH-Z19 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.

ComposantPhotoRôlePrix approx.Lien
ESP32-H2 Super MiniESP32-H2 Super MiniMicrocontrôleur principal5€AliExpress
MH-Z19Capteur CO2 MH-Z19Capteur CO₂ NDIR (infrarouge)12€AliExpress
LED RGB diffuse cathode communeLED RGBIndicateur visuel qualité d’air0.50€AliExpress
Résistances 330Ω (×3)Résistances 330ΩLimitation courant LED RGB0.30€AliExpress
Carte PCB prototype 4×6PCB prototypeSupport de soudure2€AliExpress
Fils silicone 22AWGFils siliconeCâblage interne3€AliExpress
Boîte acrylique G8DFBoîte acryliqueBoîtier de protection1€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 LEDSignification
≤ 800 ppm🟢 VertAir de bonne qualité
801–1200 ppm🟡 JauneQualité moyenne / VMC petite vitesse
1201–1500 ppm🔴 RougeAération conseillée / VMC grande vitesse
> 1500 ppm🔴 Rouge clignotantConcentration é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.

Schéma de câblage ESP32-H2, MH-Z19 et LED RGB

Connexions

ESP32-H2MH-Z19LED RGB
GPIO10 (RX)TX—
GPIO11 (TX)RX—
5VVCC—
GNDGNDCathode 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.

Carte électronique assemblée avec MH-Z19B

Réalisé avec un MH-Z19B — soudures apparentes sur le dessus.

Version avec MH-Z1911A — soudures invisibles

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.

LED RGB éclairée en fonctionnement

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.

Capteur CO2 découvert dans Home Assistant

Carte Lovelace

Voici un exemple de carte Lovelace pour afficher le CO₂ sous forme de jauge :

Carte Lovelace dans Home Assistant
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.

Licence : MIT — Publié le

💬 Discussions & Questions

0 / 5000

📬 Recevoir la newsletter hebdo

Signaler ce contenu