[Concours] VMC double flux au doigt et à l'oeil

j’ai suivi ton conseille et utilisé Chat GPT, et je pense que je suis arrivé a un truc bien, il manqueras plus que le bypass.

Si tu veux je te posterai la config esp et celle de HA

Bonjour

Super que tu y sois arrivé, oui pour.poster ta solution :wink:

Alors voici, le bypass n’est pas encore géré, car il est soit grillé soit grippé chez moi, donc pour le moment je l’ai exclu.

Voici un petit resumé des fonctions faites par l’IA :

Ce code pilote une VMC double flux Helios avec un ESP32, intégrée à Home Assistant.
Il gère :

  • la vitesse des deux ventilateurs (extraction et insufflation),

  • la connection par cable Ethernet

  • la sécurité (watchdog),

  • les températures, débits, RPM,

  • le suivi du filtre,

  • les temps de fonctionnement,

  • l’alimentation électrique,

  • les relais et le bypass,

  • les notifications Home Assistant.

J’ai rajouté un ADS1115 pour pouvoir mesurer les PWM des ventilateurs.

esphome:
  name: vmc-double-flux-helios
  friendly_name: VMC Double Flux Helios

# --------------------------------------------------
# 🔹 DÉMARRAGE SÉCURISÉ
# Initialisation des PWM et relais au boot
# --------------------------------------------------
  on_boot:
    priority: 600
    then:
      - globals.set:
          id: pwm_extraction_mem
          value: "0.25"
      - globals.set:
          id: pwm_insertion_mem
          value: "0.25"
      - output.set_level:
          id: pwm_extraction
          level: 0.25
      - output.set_level:
          id: pwm_insertion
          level: 0.25
      - switch.turn_off: relais1
      - switch.turn_off: relais2

# --------------------------------------------------
# 🔹 HEURE SYNCHRONISÉE DEPUIS HOME ASSISTANT
# Pour les calculs de filtre, bypass et watchdog
# --------------------------------------------------
time:
  - platform: homeassistant
    id: ha_time

# --------------------------------------------------
# 🔹 CONFIGURATION ESP32 ET LOGGING
# --------------------------------------------------
esp32:
  board: esp32dev
  framework:
    type: arduino

logger:
  level: DEBUG

api:
  encryption:
    key: "G0+Rc2vukJs1vbwjWauQeknF/9y+hYYJoBosPZKkgrI="

ota:
  - platform: esphome
    password: "355962038079304261cb35e074746477"

# --------------------------------------------------
# 🔹 ADS1115 - Mesure tensions 
# --------------------------------------------------
i2c:
  - id: i2c_ads
    sda: GPIO02
    scl: GPIO04
    scan: true

ads1115:
  - id: 'ads1115_0'
    address: 0x48
    continuous_mode: true
   
# --------------------------------------------------
# 🔹 ETHERNET W5500
# --------------------------------------------------
ethernet:
  type: w5500
  clk_pin: GPIO18
  mosi_pin: GPIO23
  miso_pin: GPIO19
  cs_pin: GPIO27
  interrupt_pin: GPIO35

# --------------------------------------------------
# 🔹 BUS 1-WIRE POUR CAPTEURS DS18B20
# --------------------------------------------------
one_wire:
  - platform: gpio
    pin: GPIO14

# --------------------------------------------------
# 🔹 VARIABLES GLOBALES
# Persistent variables, watchdog, bypass, temps de fonctionnement, historique filtre
# --------------------------------------------------
globals:
  # PWM mémorisés et précédents
  - id: pwm_extraction_mem
    type: float
    restore_value: true
    initial_value: "0.25"
  - id: pwm_insertion_mem
    type: float
    restore_value: true
    initial_value: "0.25"
  - id: last_pwm_extraction
    type: float
    restore_value: true
    initial_value: "0.25"
  - id: last_pwm_insertion
    type: float
    restore_value: true
    initial_value: "0.25"

  # Watchdog
  - id: watchdog_active
    type: bool
    restore_value: true
    initial_value: "false"
  - id: watchdog_counter_extraction
    type: int
    restore_value: true
    initial_value: "0"
  - id: watchdog_counter_insertion
    type: int
    restore_value: true
    initial_value: "0"
  - id: watchdog_triggered_count
    type: int
    restore_value: true
    initial_value: "0"

  # Historique Watchdog
  - id: watchdog_history_1
    type: long
    restore_value: true
    initial_value: "0"
  - id: watchdog_history_2
    type: long
    restore_value: true
    initial_value: "0"
  - id: watchdog_history_3
    type: long
    restore_value: true
    initial_value: "0"
  - id: watchdog_history_4
    type: long
    restore_value: true
    initial_value: "0"
  - id: watchdog_history_5
    type: long
    restore_value: true
    initial_value: "0"
  - id: watchdog_history_6
    type: long
    restore_value: true
    initial_value: "0"
  - id: watchdog_history_7
    type: long
    restore_value: true
    initial_value: "0"
  - id: watchdog_history_8
    type: long
    restore_value: true
    initial_value: "0"
  - id: watchdog_history_9
    type: long
    restore_value: true
    initial_value: "0"
  - id: watchdog_history_10
    type: long
    restore_value: true
    initial_value: "0"

  # Filtre VMC
  - id: filtre_last_changed
    type: int
    restore_value: true
    initial_value: "0"

  # Temps de fonctionnement ventilateurs
  - id: uptime_extraction
    type: float
    restore_value: true
    initial_value: "0.0"
  - id: uptime_insertion
    type: float
    restore_value: true
    initial_value: "0.0"

# --------------------------------------------------
# 🔹 SORTIES PWM
# --------------------------------------------------
output:
  - platform: ledc
    id: pwm_extraction
    pin: 16
    frequency: 10000 Hz
    channel: 0
    zero_means_zero: true
    min_power: 0
    max_power: 1
  - platform: ledc
    id: pwm_insertion
    pin: 17
    frequency: 10000 Hz
    channel: 5
    zero_means_zero: true
    min_power: 0
    max_power: 1

# --------------------------------------------------
# 🔹 SLIDERS PWM
# Pour commande manuelle via Home Assistant
# --------------------------------------------------
number:
  - platform: template
    name: "PWM Extraction"
    id: pwm_extraction_slider
    min_value: 0
    max_value: 100
    step: 1
    unit_of_measurement: "%"
    mode: slider
    initial_value: "25"
    restore_value: true
    optimistic: true
    set_action:
      - if:
          condition:
            lambda: 'return id(watchdog_active);'
          then:
            - number.set:
                id: pwm_extraction_slider
                value: 0
          else:
            - globals.set:
                id: pwm_extraction_mem
                value: !lambda "return x / 100.0;"
            - globals.set:
                id: last_pwm_extraction
                value: !lambda "return x / 100.0;"
            - output.set_level:
                id: pwm_extraction
                level: !lambda "return id(pwm_extraction_mem);"
            - component.update: pwm_extraction_actual

  - platform: template
    name: "PWM Insertion"
    id: pwm_insertion_slider
    min_value: 0
    max_value: 100
    step: 1
    unit_of_measurement: "%"
    mode: slider
    initial_value: "25"
    restore_value: true
    optimistic: true
    set_action:
      - if:
          condition:
            lambda: 'return id(watchdog_active);'
          then:
            - number.set:
                id: pwm_insertion_slider
                value: 0
          else:
            - globals.set:
                id: pwm_insertion_mem
                value: !lambda "return x / 100.0;"
            - globals.set:
                id: last_pwm_insertion
                value: !lambda "return x / 100.0;"
            - output.set_level:
                id: pwm_insertion
                level: !lambda "return id(pwm_insertion_mem);"
            - component.update: pwm_insertion_actual

# --------------------------------------------------
# 🔹 CAPTEURS
# Températures, débits, RPM, heures de fonctionnement, filtre, tension ADC
# --------------------------------------------------
sensor:
  # PWM réels
  - platform: template
    name: "PWM Extraction Réel"
    id: pwm_extraction_actual
    unit_of_measurement: "%"
    update_interval: 2s
    lambda: |-
      return id(pwm_extraction_mem) * 100.0;

  - platform: template
    name: "PWM Insertion Réel"
    id: pwm_insertion_actual
    unit_of_measurement: "%"
    update_interval: 2s
    lambda: |-
      return id(pwm_insertion_mem) * 100.0;

  # Températures VMC (DS18B20)
  - platform: dallas_temp
    address: 0x250000005174e128
    name: "Température Air Rejetée"
    id: vmc_temp_air_rejetee
    update_interval: 10s

  - platform: dallas_temp
    address: 0x7a00000054422228
    name: "Température Air Insufflée"
    id: vmc_temp_air_insuflee
    update_interval: 10s

  - platform: dallas_temp
    address: 0xe2000000509b4928
    name: "Température Air Extérieur"
    id: vmc_temp_air_exterieur
    update_interval: 10s

  - platform: dallas_temp
    address: 0xdf00000050780d28
    name: "Température Air Repris"
    id: vmc_temp_air_repris
    update_interval: 10s

  # Débits ventilateurs (calculé depuis PWM)
  - platform: template
    name: "Débit Ventilateur Extraction"
    id: vmc_debit_ventilation_extraction
    unit_of_measurement: "m3/h"
    icon: "mdi:fan"
    update_interval: 2s
    lambda: |-
      return id(pwm_extraction_mem) * 315.0;

  - platform: template
    name: "Débit Ventilateur Insertion"
    id: vmc_debit_ventilation_insertion
    unit_of_measurement: "m3/h"
    icon: "mdi:fan-chevron-down"
    update_interval: 2s
    lambda: |-
      return id(pwm_insertion_mem) * 315.0;

  # RPM avec watchdog
  - platform: pulse_counter
    pin:
      number: 21
      mode:
        input: true
        pullup: true
    name: "RPM Ventilateur Extraction"
    id: fan_speed_extraction
    unit_of_measurement: "RPM"
    update_interval: 2s
    filters:
      - multiply: 1
    on_value:
      then:
        - lambda: |-
            if (id(watchdog_active)) return;
            float pwm = id(pwm_extraction_mem) * 100.0;
            float rpm = id(fan_speed_extraction).state;
            if (pwm >= 20.0 && rpm < 200.0) id(watchdog_counter_extraction)++;
            else id(watchdog_counter_extraction) = 0;
            if (id(watchdog_counter_extraction) >= 1) {
              id(watchdog_active) = true;
              id(pwm_extraction_mem) = 0.0;
              id(pwm_insertion_mem) = 0.0;
              id(pwm_extraction).set_level(0.0);
              id(pwm_insertion).set_level(0.0);
              id(pwm_extraction_slider).publish_state(0);
              id(pwm_insertion_slider).publish_state(0);
            }

  - platform: pulse_counter
    pin:
      number: 22
      mode:
        input: true
        pullup: true
    name: "RPM Ventilateur Insertion"
    id: fan_speed_insertion
    unit_of_measurement: "RPM"
    update_interval: 2s
    filters:
      - multiply: 1
    on_value:
      then:
        - lambda: |-
            if (id(watchdog_active)) return;
            float pwm = id(pwm_insertion_mem) * 100.0;
            float rpm = id(fan_speed_insertion).state;
            if (pwm >= 20.0 && rpm < 200.0) id(watchdog_counter_insertion)++;
            else id(watchdog_counter_insertion) = 0;
            if (id(watchdog_counter_insertion) >= 1) {
              id(watchdog_active) = true;
              id(pwm_extraction_mem) = 0.0;
              id(pwm_insertion_mem) = 0.0;
              id(pwm_extraction).set_level(0.0);
              id(pwm_insertion).set_level(0.0);
              id(pwm_extraction_slider).publish_state(0);
              id(pwm_insertion_slider).publish_state(0);
            }

  # Heures de fonctionnement
  - platform: template
    name: "Heures Ventilateur Extraction"
    unit_of_measurement: "h"
    update_interval: 30s
    lambda: 'return id(uptime_extraction);'

  - platform: template
    name: "Heures Ventilateur Insertion"
    unit_of_measurement: "h"
    update_interval: 30s
    lambda: 'return id(uptime_insertion);'

# --------------------------------------------------
# 🔹 ADS1115 - Mesure tensions via pont diviseur + Bruit
# --------------------------------------------------
  # Mesures principales
  - platform: ads1115
    ads1115_id: ads1115_0
    multiplexer: 'A0_GND'
    gain: 2.048          # ±2.048V adapté pour ton pont diviseur
    name: "Alim PWM"
    id: voltage1
    update_interval: 1s
    filters:
      - multiply: 16.00  # Facteur pour pont diviseur 220k/15k
      - median:
          window_size: 10
      - sliding_window_moving_average:
          window_size: 10
          send_every: 1

  - platform: ads1115
    ads1115_id: ads1115_0
    multiplexer: 'A1_GND'
    gain: 2.048
    name: "Alim PWM Extraction"
    id: voltage2
    update_interval: 1s
    filters:
      - multiply: 16.1
      - median: 
          window_size: 40
      - sliding_window_moving_average:
          window_size: 20
          send_every: 1

  - platform: ads1115
    ads1115_id: ads1115_0
    multiplexer: 'A2_GND'
    gain: 2.048
    name: "Alim PWM Insertion"
    id: voltage3
    update_interval: 1s
    filters:
      - multiply: 16.1
      - median: 
          window_size: 40
      - sliding_window_moving_average:
          window_size: 20
          send_every: 1

  # --------------------------------------------------
  # 🔹 Filtre VMC
  # Jours restants et statut
  # --------------------------------------------------
  - platform: template
    name: "Filtre VMC - Jours restants"
    id: filtre_vmc_days
    unit_of_measurement: "jours"
    icon: mdi:filter
    update_interval: 30s
    lambda: |-
      if (id(filtre_last_changed) == 0) return -1;
      int delta = (id(ha_time).now().timestamp - id(filtre_last_changed)) / 86400;
      int jours = 180 - delta;
      if (jours < 0) jours = 0;
      return jours;

  - platform: template
    name: "Statut Filtre VMC"
    id: filtre_vmc_status
    update_interval: 30s
    lambda: |-
      if (id(filtre_last_changed) == 0) return 0;
      int delta = (id(ha_time).now().timestamp - id(filtre_last_changed)) / 86400;
      int jours = 180 - delta;
      if (jours <= 0) return 2;  
      if (jours <= 30) return 1;  
      return 3;
    
# --------------------------------------------------
# 🔹 INTERVALS
# Gestion du redémarrage progressif, comptage uptime et ouverture automatique du bypass
# --------------------------------------------------
interval:
  # 🔹 Redémarrage progressif PWM après reset watchdog
  - interval: 500ms
    then:
      - if:
          condition: 
            lambda: 'return !id(watchdog_active) && id(pwm_extraction_mem) < id(last_pwm_extraction);'
          then:
            - lambda: |-
                id(pwm_extraction_mem) += 0.01;
                if (id(pwm_extraction_mem) > id(last_pwm_extraction)) id(pwm_extraction_mem) = id(last_pwm_extraction);
                id(pwm_extraction).set_level(id(pwm_extraction_mem));
                id(pwm_extraction_slider).publish_state(id(pwm_extraction_mem) * 100.0);
      - if:
          condition: 
            lambda: 'return !id(watchdog_active) && id(pwm_insertion_mem) < id(last_pwm_insertion);'
          then:
            - lambda: |-
                id(pwm_insertion_mem) += 0.01;
                if (id(pwm_insertion_mem) > id(last_pwm_insertion)) id(pwm_insertion_mem) = id(last_pwm_insertion);
                id(pwm_insertion).set_level(id(pwm_insertion_mem));
                id(pwm_insertion_slider).publish_state(id(pwm_insertion_mem) * 100.0);

  # 🔹 Incrément temps de fonctionnement des ventilateurs
  - interval: 60s
    then:
      - lambda: |-
          if (id(pwm_extraction_mem) > 0.0) id(uptime_extraction) += 1.0/60.0;
          if (id(pwm_insertion_mem) > 0.0) id(uptime_insertion) += 1.0/60.0;

# --------------------------------------------------
# 🔹 TEXT SENSORS
# Informations textuelles pour Home Assistant
# --------------------------------------------------
text_sensor:
  # Date du dernier changement de filtre
  - platform: template
    name: "Date dernier changement filtre"
    id: filtre_date
    lambda: |-
      if (id(filtre_last_changed) == 0) return {"Jamais"};
      time_t t = id(filtre_last_changed);
      char buf[20];
      strftime(buf, sizeof(buf), "%d/%m/%Y", localtime(&t));
      return {buf};

  # Watchdog déclenché (date/heure)
  - platform: template
    name: "Watchdog Déclenché - Date/Heure"
    id: watchdog_triggered_time
    lambda: |-
      if (!id(watchdog_active)) return {"Non déclenché"};
      time_t t = id(ha_time).now().timestamp;
      char buf[20];
      strftime(buf, sizeof(buf), "%d/%m/%Y %H:%M:%S", localtime(&t));
      return {buf};

  # Historique Watchdog
  - platform: template
    name: "Historique Watchdog"
    id: watchdog_history_text
    lambda: |-
      std::string result = "";
      long hist[10] = {
        id(watchdog_history_1), id(watchdog_history_2), id(watchdog_history_3),
        id(watchdog_history_4), id(watchdog_history_5), id(watchdog_history_6),
        id(watchdog_history_7), id(watchdog_history_8), id(watchdog_history_9),
        id(watchdog_history_10)
      };
      for(int i=0; i<10; i++){
        if(hist[i] == 0) continue;
        time_t tt = (time_t) hist[i];
        char buf[20];
        strftime(buf, sizeof(buf), "%d/%m/%Y %H:%M:%S", localtime(&tt));
        result += buf;
        result += "\n";
      }
      if(result == "") return {"Aucun"};
      return {result.c_str()};

# --------------------------------------------------
# 🔹 BOUTONS
# Commandes manuelles pour watchdog, filtre, ventilateurs et bypass
# --------------------------------------------------
button:
  - platform: template
    name: "Reset Watchdog VMC"
    icon: mdi:restart-alert
    on_press:
      - globals.set:
          id: watchdog_active
          value: "false"
      - globals.set:
          id: watchdog_counter_extraction
          value: "0"
      - globals.set:
          id: watchdog_counter_insertion
          value: "0"

  - platform: template
    name: "Réinitialiser filtre VMC"
    icon: mdi:filter-plus
    on_press:
      - globals.set:
          id: filtre_last_changed
          value: !lambda "return id(ha_time).now().timestamp;"
      - component.update: filtre_vmc_days
      - component.update: filtre_vmc_status
      - component.update: filtre_date

  - platform: template
    name: "Reset Compteurs Ventilateurs"
    icon: mdi:restart
    on_press:
      - globals.set:
          id: uptime_extraction
          value: "0.0"
      - globals.set:
          id: uptime_insertion
          value: "0.0"

  - platform: template
    name: "Reset Historique Watchdog"
    icon: mdi:shield-refresh
    on_press:
      - lambda: |-
          id(watchdog_history_1) = 0;
          id(watchdog_history_2) = 0;
          id(watchdog_history_3) = 0;
          id(watchdog_history_4) = 0;
          id(watchdog_history_5) = 0;
          id(watchdog_history_6) = 0;
          id(watchdog_history_7) = 0;
          id(watchdog_history_8) = 0;
          id(watchdog_history_9) = 0;
          id(watchdog_history_10) = 0;

# --------------------------------------------------
# 🔹 NOTIFICATIONS HOME ASSISTANT
# Watchdog et filtre
# --------------------------------------------------
binary_sensor:
  - platform: template
    name: "Watchdog Déclenché"
    id: ha_watchdog_notify
    lambda: 'return id(watchdog_active);'
    device_class: problem
    on_press:
      then:
        - lambda: |-
            // Shift historique watchdog
            id(watchdog_history_10) = id(watchdog_history_9);
            id(watchdog_history_9)  = id(watchdog_history_8);
            id(watchdog_history_8)  = id(watchdog_history_7);
            id(watchdog_history_7)  = id(watchdog_history_6);
            id(watchdog_history_6)  = id(watchdog_history_5);
            id(watchdog_history_5)  = id(watchdog_history_4);
            id(watchdog_history_4)  = id(watchdog_history_3);
            id(watchdog_history_3)  = id(watchdog_history_2);
            id(watchdog_history_2)  = id(watchdog_history_1);
            id(watchdog_history_1)  = id(ha_time).now().timestamp;

  - platform: template
    name: "Watchdog Déclenché - Notification"
    id: ha_watchdog_notify_auto
    lambda: 'return id(watchdog_active);'
    device_class: problem
    on_press:
      then:
        - homeassistant.service:
            service: notify.notify
            data:
              title: "VMC Double Flux"
              message: "Watchdog VMC déclenché ! Vérifiez les ventilateurs."

  - platform: template
    name: "Filtre VMC - Attention"
    id: filtre_vmc_warn
    lambda: |-
      if (id(filtre_last_changed) == 0) return false;
      int delta = (id(ha_time).now().timestamp - id(filtre_last_changed)) / 86400;
      int jours = 180 - delta;
      return jours <= 30;
    device_class: problem
    on_press:
      then:
        - homeassistant.service:
            service: notify.notify
            data:
              title: "VMC Double Flux"
              message: "Le filtre VMC a moins de 30 jours avant remplacement."

# --------------------------------------------------
# 🔹 RELAIS ET BYPASS
# --------------------------------------------------
switch:
  # Relais physiques
  - platform: gpio
    id: relais1_gpio
    pin: 32
    internal: true
  - platform: gpio
    id: relais2_gpio
    pin: 33
    internal: true
  - platform: gpio
    id: relais3
    pin: 25
    name: "Relais 3"
  - platform: gpio
    id: relais4
    pin: 26
    name: "Relais 4"

  # Relais sécurisés (évite de fermer les deux relais du bypass en même temps)
  - platform: template
    name: "Relais 1 Sécurisé"
    id: relais1
    turn_on_action:
      - switch.turn_off: relais2_gpio
      - delay: 100ms
      - switch.turn_on: relais1_gpio
    turn_off_action:
      - switch.turn_off: relais1_gpio

  - platform: template
    name: "Relais 2 Sécurisé"
    id: relais2
    turn_on_action:
      - switch.turn_off: relais1_gpio
      - delay: 100ms
      - switch.turn_on: relais2_gpio
    turn_off_action:
      - switch.turn_off: relais2_gpio

Pour le moment, il faut que je crée les automatisation, et un dashboard.

Si une personne vois des choses a modifier, quelle n’hésite pas

Super intéressant ce projet ! J’ai la même VMC Atlantic que @cocof, avec un puits canadien dont le fonctionnement pourrait sûrement être optimisé avec un peu de domotique. Mais je ne suis pas très compétent en électronique, donc j’essaie d’abord de comprendre avant de me lancer.

Il y a un point qui m’interpelle : pourquoi un convertisseur logique 3,3V ↔ 5V alors que l’alimentation des moteurs délivre du 0-10V ? Est-ce que cela fonctionne simplement parce que le composant utilisé dans le convertisseur (le MOSFET BSS138) supporte en pratique 10V côté HV, ou y a-t-il une autre raison ?

Oui il s’agit d’un convertisseur 3v vers 5 mais je m’en sert comme un convertisseur 3 v vers 10v. Le pwm est bien en 0-10v