HA-Mesure Qualité de l’air PM1.0-PM2.5-M10.0

Intro

Dans un article précédent, j’abordais le détournement du boitier de mesure de qualité de l’air Vindriktning commercialisé par IKEA. Celui ci est équipé d’un capteur Pm2.5 pm1006 qui fonctionne correctement mais la gamme de mesure se limite au PM2.5 et la précision semble t’il n’est pas au rendez vous.

En parcourant les sensors « Air Quality » de EspHome, j’ai découvert le PMS5003 qui propose lui une étendue de mesures bien supérieure pour un prix raisonnable. J’en ai profité pour y associer un MHZ-19B qui lui mesure le taux de CO2 plus un afficheur local et un bandeau de 8 leds.

L’ensemble est raccordé à un ESP32 Mini qui assure une puissance de traitement supérieure à un ESP8266.

Enfin j’ai intégré le tout sur un joli circuit imprimé dont j’ai sous traité la fabrication à https://jlcpcb.com/ pour un rapport qualité prix très attrayant.

Descriptif matériel

Capteur de particules PMS 5003

Mesure fiable et en temps réel des concentrations de poussières PM2,5 ! Ce capteur utilise la diffusion laser pour rayonner les particules en suspension dans l’air, puis recueille la lumière diffusée pour obtenir la courbe de la variation de la lumière diffusée dans le temps. Le microprocesseur calcule le diamètre équivalent des particules et le nombre de particules de diamètre différent par unité de volume.

    Concentrations de PM1,0, PM2,5 et PM10,0 dans les unités standard et environnementales
    Particules par 0,1 L d’air, classées en bacs de 0,3 um, 0,5 um, 1,0 um, 2,5 um, 5,0 um et 10 um.

   Plage de mesure : 0,3~1,0;1.0~2,5;2.5~10 μ m
    Portée effective : 0~500 μ g/m³
    Portée maximale : * ≥1000 μ g/m³
    Résolution : 1 μ g/m³
    Volume standard : 0.1L
    Alimentation en courant continu : Typ:5.0 / Min:4.5 / Max : 5.5V
    Courant actif : ≤100 mA
    Courant de veille : ≤200 μ μ A
    Plage de température de fonctionnement : -10~+60 ℃
    Plage d’humidité de travail : 0~99%
    Plage de température de stockage : -40~+80 ℃
    Dimensions du module capteur : 50 × 38 × 21mm
    Poids (câble et adaptateur inclus) : 42.2g

Fiche technique

  • Sur le PMS-5003:
    • PMS-5003 <–> ESP32 Mini
    • Validation <–> SET (IO27)
    • TX <–> RX (IO25)
    • GND <–> GND
    • VCC (+5v) <–> VCC

Capteur de Co2

Le capteur Co2 est un MH-Z19B qui mesure la teneur de CO2 sur une échelle de 400 à 10000 ppm et communique en UART.

  • Sur le MH-Z19:
    • MH-Z19 <–> ESP32 Mini
    • TX <–> RX (IO16)
    • RX <–> TX (IO17)
    • GND <–> GND
    • VCC (+5v) <–> VCC

ESP32 Mini

J’ai choisi un ESP32 Mini, car l’ESP8266 est trop juste en puissance de calcul si on utilise l’intégration ESPHOME plus un afficheur plus un bandeau Led, j’ai galéré avec, je ne le recommande pas.

Afficheur Oled

Vous en trouverez sans problème sur Amazon ou en chine

L’afficheur utilise une police de caractère « Arial », vous pouvez télécharger le fichier arial.ttf ici puis le transférer dans un dossier « fonts » de votre « /config/esphome ». Bien entendu vous pouvez utiliser une autre police de caractéres, « size » détermine la taille des caractères.

Bandeau led

Il n’est pas indispensable mais il permet de visualiser rapidement d’un simple coup d’œil le niveau de pollution.

Stick NeoPixel RGB 8 leds

Mise en oeuvre

j’utilise le logiciel Eagle d’Autodesk pour la création des circuits imprimé, puis je sous traite la fabrication chez https://jlcpcb.com/ pour un rapport qualité prix très attrayant et un rendu « professionnel »

Schéma Electronique

Circuit imprimé

Mise en boitier

Cela reste à faire, je cherche le boitier idéal!

Logiciel

J’ai raccordé un bouton poussoir me permettant ainsi de faire défiler différentes pages de mesure sur l’ecran Oled.

Code ESP Home

substitutions:
  device_name: esp138-qualite-air
  adress_ip: "192.168.0.138"
  friendly_name: esp138
  time_timezone: "Europe/Paris"
  
esphome:
  name: ${device_name}
  platform: ESP32
  board: mhetesp32minikit
  #board: wemos_d1_mini32
  
  platformio_options:
    lib_deps: NeoPixelBus@2.6.0
  on_boot:
    then:
      - light.control:
          id: rgb_led
          brightness: 0.25
          state: on
wifi:
  networks:
    - ssid: !secret wifi_mi4
      password: !secret mdpwifi_mi4
      priority: 2   

    - ssid: !secret wifi_esp
      password: !secret mdpwifi_esp
      priority: 1
      
    - ssid: !secret wifi
      password: !secret mdpwifi
      priority: 0
  reboot_timeout: 5min

  manual_ip:
    static_ip: ${adress_ip}
    gateway: 192.168.0.1
    subnet: 255.255.255.0

# Enable logging
logger:
  baud_rate: 0

# Enable Home Assistant API
api:

ota:

web_server:
  port: 80
  
font:
  - file: "fonts/arial.ttf"
    id: arial
    size: 15

# Led WS2812 RGB  
light:

  - platform: neopixelbus
    num_leds: 8 
    pin: GPIO4 #gpio0
    name: "RGB strip"
    variant: ws2812
    id: rgb_led
    default_transition_length: 0s  

    
i2c:
  sda: GPIO21 #
  scl: GPIO22 #
  scan: True
  id: bus_a
  
uart:
  - id: uart_a
    rx_pin: GPIO25 #
    baud_rate: 9600

  - id: uart_b
    rx_pin: GPIO16 # GPIO16
    tx_pin: GPIO17 # GPIO17
    baud_rate: 9600
#    debug:
#      direction: BOTH
    
globals:
  - id: bp_mode
    type: int
    initial_value: '1'
    
sensor:
  - platform: pmsx003
    uart_id: uart_a
    type: PMSX003
    pm_1_0:
      name: "Concentration de particules <1.0µm"
      id: pm1_0
      accuracy_decimals: 0
      filters:
        - sliding_window_moving_average:
            window_size: 15
            send_every: 10
            send_first_at: 1

    pm_2_5:
      name: "Concentration de particules <2.5µm"
      id: pm2_5
      accuracy_decimals: 0
      filters:
        - sliding_window_moving_average:
            window_size: 15
            send_every: 10
            send_first_at: 1

      on_value:
  # de 0 à 7 / 8 leds    
  # Led 0
          - if:
              condition:
                sensor.in_range:
                  id: pm2_5
                  below: 25
              then: 
                - light.addressable_set:
                    id: rgb_led
                    range_from: 0
                    range_to: 0
                    red: 0%
                    green: 100%
                    blue: 0%
                    color_brightness: 100%
                - light.addressable_set:
                    id: rgb_led
                    range_from: 1
                    range_to: 7
                    red: 0%
                    green: 0%
                    blue: 0%
                    color_brightness: 0%  
  # led 1                  
          - if:
              condition:
                sensor.in_range:
                  id: pm2_5
                  above: 25 
                  below: 35 
              then: 
                - light.addressable_set:
                    id: rgb_led
                    range_from: 0
                    range_to: 1
                    red: 0%
                    green: 100%
                    blue: 0%
                    color_brightness: 100%
                - light.addressable_set:
                    id: rgb_led
                    range_from: 2
                    range_to: 7
                    red: 0%
                    green: 0%
                    blue: 0%
                    color_brightness: 0%  
  # Led 2              
          - if:
              condition:
                sensor.in_range:
                  id: pm2_5
                  above: 35
                  below: 45
              then: 
                - light.addressable_set:
                    id: rgb_led
                    range_from: 0
                    range_to: 2
                    red: 0%
                    green: 100%
                    blue: 0%
                    color_brightness: 100%
                - light.addressable_set:
                    id: rgb_led
                    range_from: 3
                    range_to: 7
                    red: 0%
                    green: 0%
                    blue: 0%
                    color_brightness: 0%     
  # led 3                  
          - if:
              condition:
                sensor.in_range:
                  id: pm2_5
                  above: 45
                  below: 55
              then: 
                - light.addressable_set:
                    id: rgb_led
                    range_from: 0
                    range_to: 3
                    red: 75%
                    green: 75%
                    blue: 0%
                    color_brightness: 100%
                - light.addressable_set:
                    id: rgb_led
                    range_from: 4
                    range_to: 7
                    red: 0%
                    green: 0%
                    blue: 0%
                    color_brightness: 0%     
  # led 4
          - if:
              condition:
                sensor.in_range:
                  id: pm2_5
                  above: 55
                  below: 65
              then: 
                - light.addressable_set:
                    id: rgb_led
                    range_from: 0
                    range_to: 4
                    red: 75%
                    green: 75%
                    blue: 0%
                    color_brightness: 100%
                - light.addressable_set:
                    id: rgb_led
                    range_from: 5
                    range_to: 7
                    red: 0%
                    green: 0%
                    blue: 0%
                    color_brightness: 0%  
  # led 5
          - if:
              condition:
                sensor.in_range:
                  id: pm2_5
                  above: 65
                  below: 75
              then: 
                - light.addressable_set:
                    id: rgb_led
                    range_from: 0
                    range_to: 5
                    red: 100%
                    green: 0%
                    blue: 0%
                    color_brightness: 100%
                - light.addressable_set:
                    id: rgb_led
                    range_from: 6
                    range_to: 7
                    red: 0%
                    green: 0%
                    blue: 0%
                    color_brightness: 0%      
  # led 6
          - if:
              condition:
                sensor.in_range:
                  id: pm2_5
                  above: 75
                  below: 85
              then: 
                - light.addressable_set:
                    id: rgb_led
                    range_from: 0
                    range_to: 6
                    red: 100%
                    green: 0%
                    blue: 0%
                    color_brightness: 100%
                - light.addressable_set:
                    id: rgb_led
                    range_from: 7
                    range_to: 7
                    red: 0%
                    green: 0%
                    blue: 0%
                    color_brightness: 0%    
  # Led 7
          - if:
              condition:
                sensor.in_range:
                  id: pm2_5
                  above: 85
              then: 
                - light.addressable_set:
                    id: rgb_led
                    range_from: 0
                    range_to: 7
                    red: 100%
                    green: 0%
                    blue: 0%
                    color_brightness: 100%

    pm_10_0:
      name: "Concentration de particules <10.0µm"
      id: pm10_0
      accuracy_decimals: 0
      filters:
        - sliding_window_moving_average:
            window_size: 15
            send_every: 10
            send_first_at: 1
            
    pm_0_3um:
      name: "Nb de particules >0.3"
      id: pm_03_um
      unit_of_measurement: "ppd" 
      accuracy_decimals: 0

    pm_0_5um:
      name: "Nb de particules >0.5"
      id: pm_05_um
      unit_of_measurement: "ppd" 
      accuracy_decimals: 0

    pm_1_0um:
      name: "Nb de particules >1.0"
      id: pm_10_um
      unit_of_measurement: "ppd" 
      accuracy_decimals: 0

    pm_2_5um:
      name: "Nb de particules >2.5"
      id: pm_25_um
      unit_of_measurement: "ppd" 
      accuracy_decimals: 0

    pm_5_0um:
      name: "Nb de particules >5.0"
      id: pm_50_um
      unit_of_measurement: "ppd" 
      accuracy_decimals: 0

    pm_10_0um:
      name: "Nb de particules >10.0"
      id: pm_100_um
      unit_of_measurement: "ppd"
      accuracy_decimals: 0

      
  - platform: wifi_signal
    name: "${friendly_name} WiFi Signal Sensor"
    update_interval: 60s

  - platform: uptime
    name: "${friendly_name} Uptime Sensor"
    update_interval: 60s
    
  - platform: mhz19
    uart_id: uart_b
    id: mh_z19
    co2:
      name: "MH-Z19 CO2 Value"
      id: co2
      accuracy_decimals: 0
      filters:
        - sliding_window_moving_average:
            window_size: 12 # moyenne sur 12 lecture
            send_every: 6 # envoi toutes les 6 lectures
            send_first_at: 1 # envoi à la premiere lecture
            
    temperature:
      name: "MH-Z19 Temperature"
      id: temp
      filters:
        - sliding_window_moving_average:
            window_size: 24
            send_every: 12
            send_first_at: 1
            
    update_interval: 10s
    automatic_baseline_calibration: false    


# Il s'agit d'un modèle de capteur qui effectue une moyenne mobile de 30 minutes de PM2,5
# il supprime les valeurs aberrantes et de rendre le contrôle plus fluide

  - platform: template
    name: "Concentration de particules <2.5µm median"
    id: pm2_5_median
    icon: mdi:chemical-weapon
    unit_of_measurement: µg/m³
    lambda: |-
      return id(pm2_5).state;
    update_interval: 60s
    filters:
      - median:
          window_size: 30
          send_every: 30
          send_first_at: 15

binary_sensor:
  - platform: gpio
    name: bp_mod
    id: button
    pin:
      number: GPIO14 # GPIO 15TMS
      inverted: true
      mode:
        input: true
        pullup: true
    on_click:
      - min_length: 25ms
        max_length: 500ms
        then:
          - lambda: |-
              if (id(bp_mode) < 3) { id(bp_mode) += 1; }
              else { id(bp_mode) = 1; }
              
  - platform: gpio
    name: bp_mod2
    id: button2
    pin:
      number: GPIO13 # TCK
      inverted: true
      mode:
        input: true
        pullup: true
        
#Etat de la connection
  - platform: status
    name: "${friendly_name} Status"

interval:
  - interval: 60s
    then:
      - switch.turn_on: val_mes
      - delay: 10s
#     - switch.turn_on: val_mes
      - switch.turn_off: val_mes
      - delay: 30s

switch:   
  - platform: gpio
    pin: 
      number: GPIO27 #
    id: val_mes
    name: "${friendly_name} Validation mesure"
    
  - platform: template
    name: "Calibration zero Mh-Z19 CO2"
    turn_on_action:
      - mhz19.calibrate_zero: mh_z19
      - switch.turn_off: co2_cal
    id: co2_cal

  - platform: restart
    name: "${friendly_name} Restart"

# gestion afficheur

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    address: 0x3C
    brightness: 100%
    lambda: |-
      if (id(bp_mode) == 1 )
        {
        it.printf(0,0,id(arial),"PM1.0=%.2f",id(pm1_0).state);
        it.printf(0,15,id(arial),"PM2.5=%.2f",id(pm2_5).state);
        it.printf(0,30,id(arial),"PM10.0=%.2f",id(pm10_0).state);
        it.printf(0,45,id(arial),"CO2=%.0f",id(co2).state);
        }
      else if (id(bp_mode) == 2 )
        {
        it.printf(0,0,id(arial),"0.3UM=%.1f",id(pm_03_um).state);
        it.printf(0,15,id(arial),"0.5UM=%.1f",id(pm_05_um).state);
        it.printf(0,30,id(arial),"1.0UM=%.1f",id(pm_10_um).state);
        it.printf(0,45,id(arial),"2.5UM=%.1f",id(pm_25_um).state);        
        }
      else if (id(bp_mode) == 3 )
        {
        it.printf(0,0,id(arial),"5.0UM=%.1f",id(pm_50_um).state);
        it.printf(0,15,id(arial),"10.0UM=%.1f",id(pm_100_um).state);  
        }

      

Affichage Local

Un exemple minimaliste.

type: entities
entities:
  - entity: sensor.concentration_de_particules_1_0um
  - entity: sensor.concentration_de_particules_10_0um
  - entity: sensor.concentration_de_particules_2_5um
  - entity: sensor.concentration_de_particules_2_5um_median
  - entity: sensor.mh_z19_co2_value
  - entity: sensor.mh_z19_temperature
  - entity: sensor.nb_de_particules_0_3
  - entity: sensor.nb_de_particules_0_5
  - entity: sensor.nb_de_particules_1_0
  - entity: sensor.nb_de_particules_10_0
  - entity: sensor.nb_de_particules_2_5
  - entity: sensor.nb_de_particules_5_0
  - entity: binary_sensor.bp_mod
  - entity: binary_sensor.bp_mod2
  - entity: automation.vmc_niv0_automatismes
title: esp138-qualite-air

Conclusion

Après quelques semaines d’expérience, les retours sont positifs, reste à valider la véracité des mesures, mais sans mesure étalon, on ne peut que faire confiance aux constructeurs.

J’ai remarqué que de temps en temps, la mesure de CO2 ne varie plus, j’active le switch « calibration zero mh-z19 ».

je l’utilise pour piloter ma VMC en cas de valeur PM2.5 ou de CO2 trop élévée.

2 Comments on “HA-Mesure Qualité de l’air PM1.0-PM2.5-M10.0”

  1. Bonjour,
    J’utilise le PMS5003 depuis pas mal d’années et au final je suis assez sceptique sur la qualité des données. En effet je possède actuellement 4 capteurs du même modèle acheté chez différents fournisseurs et aucun ne délivre les mêmes valeurs dans des conditions d’utilisations identiques.
    J’ai également récupéré les données d’Airparif pour avoir une référence mais la corrélation entre Airparif et PMS5003 est très mauvaise (ceci dit, on ne peut rien en conclure, car la concentration en particules fines varie beaucoup selon le lieu et je ne sais pas où se trouve la station d’Airparif).
    Donc, au final, en l’absence de données d’un capteur étalon, je suis dans l’incapacité de conclure sur la qualité du PMS5003.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *