Encore un autre Routeur Solaire pour chauffe-eau et chauffage

Principe de base

Afin d'optimiser l'auto-consommation de l’électricité produite par nos panneaux solaires, mieux vaut utiliser l’inertie de certains équipements électriques lorsque l'on a plus d’énergie que nécessaire, plutôt que de la perdre et de la racheter en heure de pointe dopée au CO2.
Si vous voulez comprendre pourquoi, c'est ICI

Faire Simple c'est dur...

il existe autant de routeur solaire que de personne ayant réfléchi au principe.
La principale difficulté étant de connaitre la puissance en Watt ( pas VA) d'injection sur le réseau en instantanée afin d'adapter au plus juste la "dérivation" (soutirage) vers nos consommateurs a forte inertie.
De toutes les solutions, la plus simple est de récupérer les informations du Linky.
Mais pour diverses raisons, certains n'ont pas de Linky, d'autre n'ont pas les informations d'injection d'activée, d'autre ...
Bref, la solution retenue se passera donc de Linky, et de ce fait sera universelle.
Le principe étant d'adapter la consommation de nos équipement a forte inertie afin que la puissance injectée soit le plus proche de 0.
La difficulté est que pour avoir une estimation juste de la puissance en Watt injectée, il faut une acquisition du courant et de la tension et tout un tas de calcul permettant de prendre en compte les harmoniques (oui les cours théoriques avec de belles sinusoïdales qui se simplifient à mort, ça reste très loin de la pratique avec tous nos appareils ...)
On peut réduire toute cette complexité et utilisant des milliers d'heures ingénieries  intégré dans un boitier à 32€ : le "Compteur d'énergie monophasé, à impulsions, avec 2 mesures bidirectionnelles jusqu'à 100 A".

Compteur d'énergie monophasé, à impulsions, avec 2 mesures bidirectionnelles jusqu'à 100 A

il n'y a plus qu'a

Maintenant c'est facile, on a 2 signaux à implusion tous les W.h qui nous informent de la consommation et de l'injection.
Il nous suffit d'adapter la puissance soutirée avant le compteur pour tendre vers 0 !
* Si je consome alors je reduis le soutirage des les équipements à intertie
* Si j'injecte, alors j'augmente le soutirage vers les équipements à intertie
Facile !

Les equipements à intertie

Cela doit être des équipements purement résistif et sans électronique :

  • chauffe-eau a thermostat mécanique.
  • radiateur type convecteur a thermostat mécanique.
  • radiateur soufflant
  • dégivrage résistif

Les équipements résistifs se pilotent facilement entre 0 et 100% avec un SSR monophasé piloté en 0-10V (prendre le dissipateur de chaleur) à 8€. Ne pas hésiter à prendre un 40A même pour piloter un équipement à 10A...

SSR monophasé piloté en 0-10V

Attention de prendre un pilotable en 0-10V. Car il existe d'autre modèles pilotables ( 4-20mA, 0-5V ... et des tout ou rien : 0-3V 0-32V DC/AC ....) 

Combinons tout cela

En 2023, pour moins de 10€ on a un micro contrôleur qui fait Wifi, programmable par USB et en OTA, intégrable à de la domotique type HomeAssistant, et programmable en YAML ! On ne vas pas s'en priver !
Il nous faut :

Il nous faudra aussi un convertisseur PWM vers 0-10V afin de piloter nos SSR.

2 sorties de routage ?

Oui ! Car à y être, cela ne coûte pas grand chose d'avoir 2 sorties.
Une première sortie prioritaire pour le chauffe-eau et une seconde non prioritaire pour un radiateur/convecteur par exemple.
La deuxième est optionnelle

Et 2 sorties relais ? 

Et comme cela ne coûtes pas grand chose de plus d'avoir 2 relais de plus, on en pilotera
- un sur la sur-consommation (plus de 400W)
- et un pour la sur-injection ( plus de 400W). Cela peut être très utile ;-)
Je m’orienterai donc vers la carte suivante :  ESP32 dual relay (à moins de 

Je m’orienterai donc vers la carte suivante :  ESP32 dual relay (à moins de 10€)

Carte ESP32 dual relay
Carte ESP32 dual relay

Le Code

Depuis ESPHome (je n'ai plus touché à l'IDE Arduino depuis que j'ai découvert ESPHome !)
Si dessous un exemple de code pour l'ESP32 dual Relay (à adapter à vos besoins).

esphome:
  name: solar-router
  friendly_name: solar_router
  on_boot:
    priority: -100
    then:
      - switch.turn_off: relay_over_Prod
      - switch.turn_off: relay_over_Conso
      - sensor.template.publish:
          id: global_pwm
          state: 0

esp32:
  board: esp32dev

# gpio26 and gpio27 -> in  (Yes 2 pin together)
# gpio33 and gpio25 -> in  (Yes 2 pin together)

# Relay 1 - gpio16
# Relay 2 - gpio17
# LED status - gpio23
# bouton - gpio0

# PWM 1 - gpio02
# PWM 2 - gpio22


# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "tototototototototo"
  # if 4h and no HomeAssistant : reboot in case of Wifi Crash ?.
  reboot_timeout: 4h

ota:
  password: "huolalalalalalla"

wifi:
  ssid: 'wifi45'
  password: 'superwpa'
  # if 4h and no Wifi : reboot in case of Wifi Crash ?.
  reboot_timeout: 4h
  manual_ip:
    # Set this to the IP of the ESP
    static_ip: 192.168.0.188
    # Set this to the IP address of the router. Often ends with .1
    gateway: 192.168.0.1
    # The subnet of the network. 255.255.255.0 works for most home networks.
    subnet: 255.255.255.0
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "SolarRouter"
    password: "anotherpassword"

captive_portal:

web_server:
  port: 80
  version: 2 # ESP8266 , set to 2 for ESP32
  local: True # ESP32

# Status LED
status_led:
  pin: GPIO23

binary_sensor:
  - platform: template
    name: "OffLine"
    id: offline
    # if no power (3Wh) or no injection (3Wh) in 10 minutes, we are OFFLINE /o\ 
    lambda: 'return id(p_in_offline).state and id(p_out_offline).state;'
    on_press:
      # And so, we stop all !
      - lambda: 'id(global_pwm).publish_state(0);' 

  - platform: copy
    name: "P_in OffLine"
    id: p_in_offline
    source_id: p_in
    internal: True
    filters: 
      # If no pulse since 10 minuts, are we OffLine ?
      - delayed_on : 600s

  - platform: copy
    name: "P_out OffLine"
    id: p_out_offline
    source_id: p_out
    internal: True
    filters: 
      # If no pulse since 10 minuts, are we OffLine ?
      - delayed_on : 600s


  - platform: gpio
    pin:
      number: GPIO26
      inverted : false
      mode:
        input: true
        pullup: true
    name: "P_in"
    id: p_in
    internal: True
    device_class : POWER 
    filters:
      - delayed_on: 500ms
    on_press:
      then:
      - component.update: w_in
      - binary_sensor.template.publish:
          id: offline
          state: OFF

  - platform: gpio
    pin:
      number: GPIO33
      inverted : false
      mode:
        input: true
        pullup: true
    name: "P_out"
    id: p_out
    internal: True
    device_class : POWER 
    filters:
      - delayed_on: 500ms
    on_press:
      then:
      - component.update: w_out
      - binary_sensor.template.publish:
          id: offline
          state: OFF
      
sensor:

  - platform: pulse_counter
    pin: 
      # use duplicate pin with same signal
      number : GPIO27
      inverted : false
      mode:
        input: true
        pullup: false
    name: "Delta P Conso (W_in)"
    id: w_in
    unit_of_measurement: 'W'
    update_interval : never
    internal_filter : 13us
    accuracy_decimals: 0
    filters:
      - multiply: 30

    # every on change set PWM | if 0 -> check W_out <=30
    on_value:
      - lambda: !lambda |-
            if (x > 0) {
              if (int(id(global_pwm).state) >= 0) { 
                id(global_pwm).publish_state(max(0,int(id(global_pwm).state) - max(1,int(x/30))));
              }
              if (id(w_out).state > 0 ) { 
                  id(w_out).publish_state(0);
              }
            }

  - platform: pulse_counter
    pin: 
      # use duplicate pin with same signal
      number : GPIO25
      inverted : false
      mode:
        input: true
        pullup: false
    name: "Delta P Inj (W_out)"
    id: w_out
    unit_of_measurement: 'W'
    update_interval : never
    internal_filter : 13us
    accuracy_decimals: 0
    filters:
      - multiply: 30
    # every on change set PWM | if 0 -> check W_out <=30 
    on_value:
      - lambda: !lambda |-
            if (x > 0) {
              if (int(id(global_pwm).state) <= 200) { 
                id(global_pwm).publish_state(min(200,int(id(global_pwm).state) + int(x/30)));
              } 
              if (id(w_in).state > 0) { 
                id(w_in).publish_state(0);
              }
            }

  - platform: template
    name: "Conso Prioritaire PWM (Actuel)"
    id: "conso_pwm1"
    accuracy_decimals: 0
    unit_of_measurement: "%"

  - platform: template
    name: "Conso Secondaire PWM (Actuel)"
    id: "conso_pwm2"
    accuracy_decimals: 0
    unit_of_measurement: "%"

  - platform: template
    name: "Injection Acutelle"
    id: "p_inj"
    accuracy_decimals: 0
    unit_of_measurement: "W"
    lambda: !lambda |-
        if (int(id(global_pwm).state) >= 200) {
          return  id(w_out).state;
        } else {
          return  0;
        }

  - platform: template
    name: "Global PWM (Actuel)"
    id: "global_pwm"
    update_interval: 60s
    accuracy_decimals: 0
    unit_of_measurement: "%"
    # on change : set 2 other PWM  0-100% and 100%-200%
    on_value:
      then:
      - lambda: !lambda |-
          id(pwm_output_high_p).set_level(float(min(100,int(x)))/100.0);
          id(pwm_output_low_p).set_level(float(min(100,max(0,int(x) - 100)))/100.0);
          id(conso_pwm1).publish_state(min(100,int(x)));
          id(conso_pwm2).publish_state(min(100,max(0,int(x) - 100)));
      - if:
          condition:
            lambda: 'return (id(w_in).state > 400) and (x==0) and ! (id(relay_over_Conso).state);'
          then:
            - logger.log: "over-consumption"
            - switch.turn_on: relay_over_Conso
      - if:
          condition:
            lambda: 'return (id(w_out).state > 400) and (x==200) and !(id(relay_over_Prod).state);'
          then:
            - logger.log: "over-production"
            - switch.turn_on: relay_over_Prod
      - if:
          condition:
            lambda: 'return (x<200) and (id(relay_over_Prod).state);'
          then:
            - switch.turn_off: relay_over_Prod
      - if:
          condition:
            lambda: 'return (x>0) and (id(relay_over_Conso).state);'
          then:
            - switch.turn_off: relay_over_Conso
    

# PWM output (software on ESP8266 to test)
output:
  - platform: ledc # ledc for ESP32 (hardware PWM) esp8266_pwm for ESP8266
    # Primary Power pin in PWM converted to 0-10V converted to 0-100% AC 230V wave
    pin: GPIO02
    frequency: 1000 Hz
    id: pwm_output_high_p
  - platform: ledc # ledc for ESP32 (hardware PWM) esp8266_pwm for ESP8266
    # Secondary Power pin in PWM converted to 0-10V converted to 0-100% AC 230V wave
    pin: GPIO22
    frequency: 1000 Hz
    id: pwm_output_low_p

switch:
  - platform: gpio
    pin:
      number: GPIO16
      inverted : false
    id: 'relay_over_Conso'
    name: "Sur-Consommation"
    restore_mode: ALWAYS_OFF

  - platform: gpio
    pin:
      number: GPIO17
      inverted : false
    id: 'relay_over_Prod'
    name: "Sur-Production"
    restore_mode: ALWAYS_OFF 

Le Schéma complet

ici il y aura le schéma complet un jour ...

Le résulat

Le module est parfaitement autonome et fonctionne avec et sans HomeAssistant !