Tracker solaire DIY perturbé par perte Wifi

Bonjour,

Présentation

Tout nouveau sur le forum, je me présente en quelques lignes avant de vous exposer mon problème.
J’ai découvert HA il y a tout juste 1 an, et j’en suis tombé amoureux. Néophyte, j’avance à mon rythme.
J’ai commencé par une installation sur mon DS218+, puis j’ai désormais migré sur un vieux PC, avec une installation Proxmox.
Rapidement je me suis tourné vers ESPHome. J’ai 2 Pi pico W et 3 ESP32 qui fonctionnent H24/7J.
Je pourrais vous faire un petit détail de mes installations si ça intéresse.
Sans entrer dans le détail :

  • ESP32 S2 mini, avec 1 DHT22, 2 LD2410C, 1 syrène, 2 relais.
    Afin d’avoir les données Temp/Hum du RDC, et gestion de la lumière automatique avec les détecteurs, et quand absent pour la gestion de l’alarme.

  • Pi pico w, avec 1 DHT, 6 relais statiques, 1 télécommande TDRC 16.
    Pour avoir les données Temp/Hum de l’entrée, et gestion des volets de la maison en automatique en utilisant la sortie des relais pour shunter les boutons de la télécommande, ainsi qu’une petite soudure pour shunter un bouton dans mon portier Legrand pour gestion de mon portail à travers HA.

  • ESP32 S2 mini dans mon garage, avec 2 relais, 1 DHT, 2 capteurs fin de course, 1 liaison UART Daly BMS, 1 liaison UART Victron, 1 sortir PWM pour servomoteur.
    Le PWM pour gérer un servomoteur pour ouvrir et fermer automatique la porte de mon poulailler, avec capteur fin de course haut. Les 2 relais pour ouvrir et ferme la porte de mon garage et capteur de fin de course haut. Le DHT pour avoir Temp/hum extérieur que j’utilise pour gestion de ma VMC et thermostat chauffage. Les liaisons UART qui proviennent de ma batterie solaire fait maison avec 8 cellules EVE 305Ah, MPPT Victron, BMS Daly onduleur,…

  • Pi pico w dans ma buanderie, avec 8 relais, 4 contacteurs 4P (2NC/2NO), 3 PZEMAC sur 1 modbus.
    2 relais pour le contact sec pour demande chauffage sur ma PAC (1 étage, 1 RDC), 2 relais pour les 2 pompes de circulation du chauffage (1 étage, 1 RDC), le tout piloté par les thermostats HA.
    4 autres relais pour piloter les 4 contacteurs de puissance 4P, qui me permettent de piloter la provenance de l’énergie de la maison (divisée en 4 zones), soit « EDF » soit le solaire à travers l’onduleur et sa batterie. Et les 3 PZEMAC pour avoir les données de puissance, tension, énergie de ce qui sort de l’onduleur, EDF et PAC.

  • En cours, conception d’une batterie sur mesure type Ecoflow, pour aller faire du camping, avec ESP32 et ESPHome standalone.

  • Enfin, le dernier ESP32 S2 Mini, sur lequel j’ai besoin de votre aide…je vous explique

Mon problème

Début aout, j’ai eu une petite semaine de tranquillité.
Je me suis lancé dans la conception d’un tracker solaire.
J’ai rapidement fait le support, les fondations et mis le tout en place.
Structure bois, avec roulements coniques pour les liaisons, 2 vérins électriques 12V pour la dynamique.





J’ai installé dans une boite étanche, 1 ESP32 S2 mini, 2 pont en H DRV8871, 1 alimentation 230VAC/12VDC, 1 abaisseur de tension DC/DC pour passer du 12VDC à 5VDC.
Le tout avec quelques couches d’époxy pour contrer l’humidité et la corrosion, en espérant que les différences de coefficient de dilatation ne dessoudent pas les composants montés en surface.
J’ai coupé l’antenne wifi de l’ESP32, et je l’ai remplacé par une antenne extérieur pour améliorer la réception wifi.
Puis j’ai coulé dans de l’époxy, une IMU MPU 6050, dans un lego cubique, qui est collée aux panneaux solaires, afin d’avoir leur orientation, avec calibration au préalable.

Pour faire très simple, je récupère les éphémérides du soleil à travers l’objet SUN, que je transforme dans un repère cartésien (x,y,z), que je transforme à nouveau en roulis/tangage.
Avec l’IMU j’ai roulis/tangage des panneaux, ainsi je peux faire 2 boucles d’asservissements pour piloter les vérins afin d’aligner les 2 vecteurs.
Je n’utilise pas les gyro, car le logiciel ne tourne pas assez vite pour les intégrations, de plus je suis quasiment sur les cardans des 2 axes de rotation, donc très peu de bruit d’accélération linéaire.

Tout fonctionne super bien sauf … quand je perd la liaison wifi, l’ESP se met en vrac et commande mes vérins de façon non désirée.

J’ai pourtant essayé de dupliquer tous les éléments ESPHome en ayant une partie en interne et l’autre externe en espérant que si la liaison wifi est inopérant, tous les éléments internes garantiraient un bon fonctionnement.

Par exemple ci-dessous, le roulis, avec à chaque perte du wifi, ce créneau vers le bas. J’ai ajouté une « protection » qui arrête l’asservissement à chaque problème, c’est pourquoi la reprise de l’asservissement est plus ou moins rapide car je dois le faire manuellement.

En violet la consigne, en bleu le réalisé.

Un petit zoom. La perte de données est toujours entre 30 et 40 secondes, avec arrêt de la boucle d’asservissement. j’ai l’impression que le timeout sur le wifi n’est pas pris en compte.

J’ai tellement essayé de configuration que je vais avoir du mal à toutes vous les détailler.
Ce n’est pas cyclique et ça dépend des jours. Mais c’est clairement lié au wifi.

Ma configuration

Je vous mets mon code ci-dessous … si jamais vous arrivez à m’aiguiller pour trouver comment faire afin d’avoir un code qui tourne quoi qu’il arrive sur la liaison wifi.

esphome:
  name: s2-tracker
  friendly_name: s2_sunTracker
  on_boot:
    priority: 600
    then:
      - switch.turn_on: manual_mode

substitutions:
  repos_tangage: '-20.0'
  repos_roulis: '-10.0'
  precision_offset: '2.0'
  offset_tangage: '0.0'
  offset_roulis: '7.0'



esp32:
  board: lolin_s2_mini
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: xxxxxxxxxxxxx

ota:
  - platform: esphome
    password: !secret s2_lumiere

web_server:
    local: False
    version: 3
    port: 80
    include_internal: True

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  reboot_timeout: 60min
  on_disconnect:
    - switch.turn_on: manual_mode


  manual_ip:
    static_ip: 192.168.1.26
    gateway: 192.168.1.1
    subnet: 255.255.255.0

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  # ap:
  #   ssid: "S2-Lumiere Fallback Hotspot"
  #   password: xxxxxx

captive_portal:

i2c:
  sda: GPIO40
  scl: GPIO38
  frequency: 50kHz
  scan: True


output:
  - platform: ledc
    id: tangage_forward_pin
    pin: GPIO39
    frequency: 50000 Hz
  - platform: ledc
    id: tangage_reverse_pin
    pin: GPIO37
    frequency: 50000 Hz
  - platform: ledc
    id: roulis_forward_pin
    pin: GPIO33
    frequency: 50000 Hz
  - platform: ledc
    id: roulis_reverse_pin
    pin: GPIO18
    frequency: 50000 Hz

fan:
  - platform: hbridge
    id: tangage_verin
    icon: mdi:sun-compass
    name: "Verin tangage"
    pin_b: tangage_forward_pin
    pin_a: tangage_reverse_pin
    decay_mode: slow
    internal: False

  - platform: hbridge
    id: roulis_verin
    icon: mdi:sun-compass
    name: "Verin roulis"
    pin_b: roulis_forward_pin
    pin_a: roulis_reverse_pin
    decay_mode: slow 
    internal: False

sensor:
  - platform: mpu6050
    address: 0x68
    accel_x:
      id: accel_x
      name: "IMU Accel X"
      internal: True
      filters:
        - clamp:
            min_value: -9.81
            max_value: 9.81
        - exponential_moving_average:
            alpha: 0.25
            send_every: 1
    accel_y:
      id: accel_y
      name: "IMU Accel Y"
      internal: True
      filters:
        - clamp:
            min_value: -9.81
            max_value: 9.81
        - exponential_moving_average:
            alpha: 0.25
            send_every: 1
    accel_z:
      id: accel_z
      name: "IMU Accel z"
      internal: True
      filters:
        - clamp:
            min_value: 0.01 #IMU à l'envers, tombé
            max_value: 10
            ignore_out_of_range: False
        - exponential_moving_average:
            alpha: 0.25
            send_every: 1
    temperature:
      name: "MPU6050 Temperature"
      id: MPU_temp
      internal: True
    update_interval: 250ms

  - platform: template
    id: PV_roulis
    name: PV roulis
    internal: True
    unit_of_measurement: °
    update_interval: 250ms
    lambda: return atan2(id(accel_y).state, id(accel_z).state)*180/PI;
    filters:
      - offset : $offset_roulis
      - clamp:
          min_value: -80.0
          max_value: 80.0
          ignore_out_of_range: False
      - timeout: 800ms
            
  - platform: template
    id: PV_tangage
    name: PV tangage
    internal: True
    unit_of_measurement: °
    update_interval: 250ms
    lambda: return atan2(-id(accel_x).state, id(accel_z).state)*180/PI;
    filters:
      - offset : $offset_tangage
      - clamp:
          min_value: -80
          max_value: 80
          ignore_out_of_range: False
      - timeout: 800ms

  - platform: template
    name: PV roulis
    unit_of_measurement: °
    accuracy_decimals: 1
    update_interval: 5s
    lambda: return id(PV_roulis).state;
    

  - platform: template
    name: PV tangage
    unit_of_measurement: °
    accuracy_decimals: 1
    update_interval: 5s
    lambda: return id(PV_tangage).state;

  - platform: template
    name: MPU température
    device_class: temperature
    update_interval: 60s
    lambda: return id(MPU_temp).state;

  - platform: template
    id: OffsetRoll
    name: Offset Roulis
    internal: True
    update_interval: 250ms
    lambda: |-
      if(isnan(id(PV_roulis).state) || isnan(id(sunRoulis).state) || id(manual_mode).state){
        return 0;
      }
      else {return id(PV_roulis).state - id(sunRoulis).state;}
    filters:
      - timeout:
          timeout: 800ms
          value: 0.0
    on_value_range:
      - below: -$precision_offset # Ecart à partir duquel on met en route l'asservissement
        then:
          - lambda: |-
              auto call = id(roulis_verin).turn_on();
              call.set_speed(100);
              call.set_direction(FanDirection::FORWARD);
              call.perform();
      - above: -$precision_offset 
        below: $precision_offset
        then:
          - fan.turn_off: roulis_verin
      - above: $precision_offset # Ecart à partir duquel on met en route l'asservissement
        then:
          - lambda: |-
              auto call = id(roulis_verin).turn_on();
              call.set_speed(100);
              call.set_direction(FanDirection::REVERSE);
              call.perform();

  - platform: template
    id: OffsetPitch
    name: Offset Tangage
    internal: True
    update_interval: 250ms
    lambda: |-
      if(isnan(id(PV_tangage).state) || isnan(id(sunTangage).state) || id(manual_mode).state){
        return 0;
      }
      else {return id(PV_tangage).state - id(sunTangage).state;}
    filters:
      - timeout:
          timeout: 800ms
          value: 0.0
    on_value_range:
      - below: -$precision_offset # Ecart à partir duquel on met en route l'asservissement
        then:
          - lambda: |-
              auto call = id(tangage_verin).turn_on();
              call.set_speed(80);
              call.set_direction(FanDirection::FORWARD);
              call.perform();
      - above: -$precision_offset 
        below: $precision_offset
        then:
          - fan.turn_off: tangage_verin
      - above: $precision_offset # Ecart à partir duquel on met en route l'asservissement
        then:
          - lambda: |-
              auto call = id(tangage_verin).turn_on();
              call.set_speed(80);
              call.set_direction(FanDirection::REVERSE);
              call.perform();

  - platform: template
    name: Sun Elevation filtre
    id: sunElevation
    internal: True
    lambda: return id(sunElev).state;
    filters: 
      - clamp:
          min_value: 41
          max_value: 89
          ignore_out_of_range: False
      

  - platform: template
    name: Sun Azimuth filtre
    id: sunAzimuth
    internal: True
    lambda: return id(sunAz).state;
    filters: 
      - clamp:
          min_value: 70
          max_value: 290
          ignore_out_of_range: False

  - platform: sun
    name: Sun Elevation
    id: sunElev
    type: elevation
    internal: False
    filters: 
      - lambda: |-
          if(isnan(x)){return {};}
          else{return x;}
      
  - platform: sun
    name: Sun Azimuth
    id: sunAz
    type: azimuth
    internal: False
    filters: 
      - lambda: |-
          if(isnan(x)){return {};}
          else{return x;}

  - platform: template
    id: sunX
    name: sunX
    internal: True
    lambda: return  cos(id(sunElevation).state*PI/180)*cos(id(sunAzimuth).state*PI/180);
    update_interval: 10s

  - platform: template
    id: sunY
    name: sunY
    internal: True
    lambda: return  -cos(id(sunElevation).state*PI/180)*sin(id(sunAzimuth).state*PI/180);
    update_interval: 10s

  - platform: template
    id: sunZ
    name: sunZ
    internal: True
    lambda: return  sin(id(sunElevation).state*PI/180);
    update_interval: 10s

  - platform: template
    id: sunRoulis
    unit_of_measurement: °
    internal: True
    update_interval: 10s
    lambda: |-
      if (id(storm_mode).state) return {$repos_roulis}; // Angle azimuth repos
      return -(-acos(id(sunY).state/sqrt( pow( id(sunY).state , 2) + pow( id(sunZ).state , 2)))*180/PI+90);

  - platform: template
    id: sunTangage
    unit_of_measurement: °
    internal: True
    update_interval: 10s
    lambda: |-
      if (id(storm_mode).state) return {$repos_tangage}; // Angle elevation repos
      return asin(id(sunX).state)/sqrt( pow( id(sunX).state , 2) + pow( id(sunZ).state , 2))*180/PI;

  - platform: template
    name: sunRoulis
    accuracy_decimals: 1
    unit_of_measurement: °
    update_interval: 10s
    lambda: return id(sunRoulis).state;
            
  - platform: template
    name: sunTangage
    accuracy_decimals: 1
    unit_of_measurement: °
    update_interval: 10s
    lambda: return id(sunTangage).state;
    
  - platform: internal_temperature
    name: "ESP Temperature"
  
  - platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB
    name: "WiFi Signal dB"
    id: wifi_signal_db
    update_interval: 60s
    entity_category: "diagnostic"

switch:
  - platform: template
    id: manual_mode
    optimistic: True
    name: "Mode manuel"
    icon: "mdi:hand-back-right-outline"
    on_turn_on:
    - fan.turn_off: tangage_verin
    - fan.turn_off: roulis_verin
    - lambda: return id(OffsetPitch).publish_state(0.0); 
    - lambda: return id(OffsetRoll).publish_state(0.0);

  - platform: template
    id: storm_mode
    name: "Mode nuit/intemperie"
    icon: "mdi:weather-lightning-rainy"
    optimistic: True
    on_turn_on:
    - lambda: return id(sunTangage).publish_state($repos_tangage); //Angle elevation repos
    - lambda: return id(sunRoulis).publish_state($repos_roulis); //Angle azimuth repos

button:
  - platform: restart
    name: "Restart"

time:
  - platform: homeassistant

sun:
  latitude: xxxxxxxxx
  longitude: xxxxxxxxxxxxxx

  on_sunrise:
    - then:
      - switch.turn_off: storm_mode
        # Asservir vers position du levée du soleil

  on_sunset:
    - then:
      - switch.turn_on: storm_mode
        # Mettre en position de repos/vent à presque horizontal, avec légère pente pour écoulement pluie

Merci beaucoup par avance, et espère ne pas être trop à côté de la plaque des règles du forum pour l’édition de ce premier message.

Au plaisir de vous lire et d’échanger,
Florent

4 « J'aime »

Je ne sais pas trop t’aider pour de vrai mais juste au cas ou, il me semble avoir vue que certains utilisaient la position à atteindre directement dans l’esp.
Si je dit pas de bêtises, en gros c’est l’esp qui connait la position a atteindre toute seul grâce à l’heure et la date.

J’y ai pensé également mais ça éloigne de la flexibilité de EspHome et HA.
Dans mon code que j’ai nettoyé, j’avais créé plein de boutons pour simuler des erreurs en publiant des NAN un peu partout, d’où l’ajout des vérifications isnan(x). Et dans le sensor qui récupère la position du soleil, s’il y a une erreur, normalement je l’ignore et je conserve la dernière valeur connue.

Sinon, je mets un 2eme ESP32, le premier qui gère tout en standalone, et le deuxième qui fait l’interface avec HA, et une liaison UART entre les deux. Mais c’est dommage.

Je continue de faire des essais dans tous les sens, il s’avère que la paramétrage de reboot_timeout ne soit pas pris en compte, ou en tout cas pas comme espéré.

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  reboot_timeout: 0s
  power_save_mode: none

J’ai mis en place de debug component, et voici ce qu’il me sort :

Reset Reason : rtc watchdog reset digital core and rtc module

ça pourrait venir d’une boucle infinie ou que le processeur pédale trop lentement.

dans la section wifi, tu peut aussi tester l’option output_power

Sinon la puissance de ton signal est constante ou elle se dégrade fortement avant de perdre le signal wifi ?
Peut être que qq chose influx sur la puissance du signal reçu ( genre, un moteur , … )

La puissance de mon signal est plutôt constante, la réception oscille entre -75 et -80dB.
Ce n’est pas très clair le ‹ output_power ›, la valeur par défaut et les bornes atteignables.
As-tu un RETEX sur le sujet ?

Demain je vais essayer de rapprocher mon routeur Wifi.
Puis je vais changer la tension de sortie de mon convertisseur DC/DC, aujourd’hui en 3V3, je vais passer en 5V.
Je vais me procurer un ESP32S3 pour l’essayer en parallèle.

Je continue d’étoffer mes recherches.

Le constituant DEBUG permet d’avoir une approximation de la charge CPU.

La boucle de l’ESP tourne en moins de 20ms, mais à un moment, sans savoir pour quelle raison, elle passe à 270ms (donc supérieur aux 250ms que je demande à certaines variables).
Puis il y a plusieurs pics à plus de 1000ms.
Puis le watchdog déclenche un reboot. Visible vers 11h38.

ça me laisse penser que le Wifi serait de moins en moins en cause, car je reçois bien toutes ces données.

Salut,
J’ai jamais trouvé comment récupérer la période de la boucle principale esphome, comment tu fais ?

Le code ci-dessous :

# Enable logging
logger:
  level: DEBUG

debug:
  update_interval: 5s

text_sensor:
  - platform: debug
    device:
      name: "Device Info"
    reset_reason:
      name: "Reset Reason"
sensor:
  - platform: debug
    loop_time:
      name: "Loop Time"

oui, perso, j’ai mis ça output_power: 20.5 pour améliorer la connexion

Ah OK j’avais vu dans la doc mais j’avais compris que ça donnait la durée max, je pensais pas que c’était mis à jour en permanence. Pas super clair je trouve :

loop_time (Optional): Reports the longest time between successive iterations of the main loop.

Je te l’accorde ce n’est pas super clair, il faut comprendre, que ça retourne la boucle la plus longue dans l’intervalle de mise à jour.

1 « J'aime »

Avec le routeur Wifi pas loin, ça fonctionne très bien. Plus aucun problème.

Malheureusement je ne peux pas le laisser à cet endroit.

J’ai flashé un ESP32 avec esp32_nat_router_extended
que j’ai positionné à proximité, connecté à mon routeur principal.
J’ai modifié le ssid et password wifi dans mon yaml, mais impossible de reprendre la main dessus.
J’ai surement une configuration reseau à faire quelque part, mais difficile de trouver où.

J’ai bien mis en place le routeur ESP32 (192.168.4.1) et configuré avec le port forwarding.

En revanche, impossible d’imposer une adresse IP statique aux clients qui s’y connectent.
Quand j’essaie de faire ça, il indique 0.0.0.0 en tant qu’IP.

Je pense que c’est une limitation de l’ESP32 S2 mini. Vous avez des idées ? car d’autres avec d’autres ESP n’ont pas ce problème. J’attends des S3 et je ferai l’essai.

Sans ça, avec un seul client (192.168.4.2), et l’IP géré par l’ESP router, j’arriverai bien à tout remonter sur HA qui est sur un autre réseau (192.168.1.x)

Une discussion intéressante: ESPHome issue with a repeater (NAT) - ESPHome - Home Assistant Community (home-assistant.io)