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)

Bonjour @Scorpix, superbe projet qui me rappelle celui que j’ai dû abandonner faute de temps :
https://forum.hacf.fr/t/traqueur-solaire/27301

Suite à l’installation d’un traqueur Eco Worthly sur lequel j’ai changer les vérins et le support des panneaux pour rigidifier la structure qui semblait un peu faible.
Maintenant, je voudrais m’attaquer au dernier problème qui est que je retrouve mes panneaux « dos au soleil » lorsque le ciel est couvert. J’ai cherché des optimisations sur le contrôleur mais rien ne corrige complètement le problème.

Je souhaitais donc virer le contrôleur d’origine pour asservir la position via un MPU6050 et une « tête d’ensoleillement » (comme celle-ci : capteur solaire). Le MPU permettrai d’interdire aux panneaux de sortir d’une zone où le capteur d’ensoleillement pourrait « optimiser » l’ensoleillement des panneaux.

Et suite à me recherche, je suis tombé sur ton post qui semble le plus abouti de tous les projets que j’ai pu trouver.

Est-ce que, depuis ce post, tu as modifier le code ?
Est-ce que tu as de la documentation qui explique le code ?
Si non, je me propose d’en rédiger une, avec ton aide, si tu es d’accord ?

@Scorpix , petite question sur ton code , comment as-tu trouver la valeur de ces variables :

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

Salut,

ça va faire maintenant une année qui le tracker fonctionne tous les jours.

Avec deux panneaux 405W, j’arrive à atteindre 7,5kWh les meilleurs journées. J’ai des masques le matin et le soir … sans quoi je pense que le 9kWh ne serait pas loin.

J’ai réussi à résoudre mes problèmes de Wifi, en tout cas c’est plus robuste face aux pertes de liaison.

Au début, j’ai eu aussi pas mal de galère avec la liaison I2C de l’IMU. Mais avec les mises à jour de Esphome et beaucoup d’essais, j’ai trouvé une solution plutôt stable.

Je suis passé de 1 à plusieurs reboot de l’ESP par jour, à maintenant 1 semaine sans devoir réinitialiser l’I2C.

Je suis entrain d’en faire une version à 3 panneaux.

J’ai eu quelques soucis de scotch double face entre l’IMU et les panneaux, quand ça chauffe l’IMU tombait. J’ai réalisé un petit support en impression 3D qui vient se caler sur le côté des panneaux.

J’ai fait une petite automatisation qui met les panneaux en mode « nuit/tempête », dès que l’alerte Météo France passe à Orange ou Rouge, sur le Vent, Pluie, Orage, … et qui la retire une fois de retour à Vert :slight_smile:

Le code a bien évolué depuis le temps, je t’en fait une description juste après :wink:

Pour ce qui est de l’ajout d’un capteur solaire en plus de l’IMU, le gain est vraiment minime, <5%. En plus de complexifier la configuration et installation.
Il faudra ajouter des filtres pour le passage des nuages.
Avec le suivi sur les données de l’IMU, tu suis le soleil quoi qu’il arrive, c’est plus simple.
Etant donné la dynamique, je n’utilise pas les gyroscopes.

Et une autre question, lorsqu’il arrive une panne de courant et que l’ESP n’est plus alimenté alors que les traqueur n’est pas en position « 0 » (roulis et tangage à 0), comment cela se passe, le MPU n’a pas besoin d’être réinitialiser avec des offset ou autre ?

substitutions:
  repos_tangage: '-30.0' #angle tangage repos mode Nuit/Tempête par rapport à l'horizontal
  repos_roulis: '-20.0' #angle roulis repos mode Nuit/Tempête par rapport à l'horizontal
  precision_offset: '2.0' #angle cône de suivi du soleil
  max_speed_offset: '12.0' #si la consigne est à plus de 12°, vitesse vérin à 100%
  offset_tangage: '0.0' #permet d'ajuster des erreurs sur le tangage
  offset_roulis: '0.0' #permet d'ajuster des erreurs sur le roulis
  boucle_asservissement: '100ms' #vitesse de la boucle d'asservissement
  offset_azimuth: '-5.0' #mon système fonctionne qui si tu as l'axe du roulis colinéaire avec l'axe Nord-Sud, ça permet d'ajuster l'erreur
  PI: '3.14159265359'
  offset_accX: '-0.35' #Calibration grossière de l'IMU, à faire sur table nivelé au tout début
  offset_accY: '0.1' #Calibration grossière de l'IMU, à faire sur table nivelé au tout début
  offset_accZ: '-0.7' #Calibration grossière de l'IMU, à faire sur table nivelé au tout début

Voici la configuration qui fonctionne avec l’IMU lié à un ESP Seeed C6

i2c:
  sda: GPIO22
  scl: GPIO23
  frequency: 400kHz #default 50kHz
  scan: False #Sinon erreur de compilation

Déclaration des 4 sorties PWM qui vont vers deux DRV8871 afin de piloter les vérins :

output:
  - platform: ledc
    id: tangage_forward_pin
    pin: GPIO18
    frequency: 30000 Hz
  - platform: ledc
    id: tangage_reverse_pin
    pin: GPIO20
    frequency: 30000 Hz
  - platform: ledc
    id: roulis_forward_pin
    pin: GPIO19
    frequency: 30000 Hz
  - platform: ledc
    id: roulis_reverse_pin
    pin: GPIO17
    frequency: 30000 Hz

Dans Esphome, le pilotage des vérins à travers un pont en H, se fait avec la classe FAN.
Option, j’ai ajouté un nombre incrémental à chaque action des vérins pour avoir une idée du nombre de fois par jour qu’ils sont mis en action. ça m’a permis aussi d’ajuster mes valeurs de filtre sur les données IMU un peu plus loin.
Les jours de vent, je suis à 700 actions par vérin.

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
    on_turn_on: 
      then:
        - number.increment: number_tangage
    
  - 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
    on_turn_on: 
      then:
        - number.increment: number_roulis

Ci-dessous, les deux boucles d’asservissement en position des vérins.
Je passe en argument dans le script, l’écart entre le soleil et le vecteur normal des panneaux.
En fonction du signe, je pilote dans un sens ou l’autre (Reverse/Forward).
En fonction de la valeur, je pilote plus ou moins la vitesse des vérins → Speed.
ça évite, pour les petits mouvements, de piloter à 100% et de mettre des gros coups dans le système. Plus tu réduis le durée de $boucle_asservissement, plus ça sera doux.
La vitesse minimale est 60%, car en dessous, les vérins ont un peu de mal dans les situations les plus délicates.
Il faut comprendre, Roll : Roulis, Pitch : Tangage.

script:
  - id: RollPilot
    mode: single
    parameters:
      offset: float
    then:
      - lambda: |-
          if (abs(offset)>$precision_offset) {
            int speed = 0;
            if(abs(offset)>$max_speed_offset) {
              speed = 100;
            }
            else {
              speed = 60 + (40*abs(offset)/$max_speed_offset);
            }
            if (offset>0) {
              auto call = id(roulis_verin).turn_on();
              call.set_speed(speed);
              call.set_direction(FanDirection::REVERSE);
              call.perform();
            } else {
              auto call = id(roulis_verin).turn_on();
              call.set_speed(speed);
              call.set_direction(FanDirection::FORWARD);
              call.perform();
            }
          }
          else {
            auto call = id(roulis_verin).turn_off();
            call.perform();
          }

  - id: PitchPilot
    mode: single
    parameters:
      offset: float
    then:
      - lambda: |-
          if (abs(offset)>$precision_offset) {
            int speed = 0;
            if(abs(offset)>$max_speed_offset) {
              speed = 100;
            }
            else {
              speed = 60 + (40*abs(offset)/$max_speed_offset);
            }
            if (offset>0) {
              auto call = id(tangage_verin).turn_on();
              call.set_speed(speed);
              call.set_direction(FanDirection::REVERSE);
              call.perform();
            } else {
              auto call = id(tangage_verin).turn_on();
              call.set_speed(speed);
              call.set_direction(FanDirection::FORWARD);
              call.perform();
            }
          }
          else {
            auto call = id(tangage_verin).turn_off();
            call.perform();
          }

Ici ce sont simplement les nombres incrémentaux pour les actions des vérins par journée, remis à zéro plus bas dans la classe SUN.

number:
  - platform: template
    id: number_roulis
    internal: True
    restore_value: True
    min_value: 0
    max_value: 9999
    step: 1
    optimistic: True
    on_value: 
      then:
        - lambda: "return id(sensor_number_roulis).publish_state(x);"
            

  - platform: template
    internal: True
    id: number_tangage
    restore_value: True
    min_value: 0
    max_value: 9999
    step: 1
    optimistic: True
    on_value: 
      then:
        - lambda: "return id(sensor_number_tangage).publish_state(x);"

Ici sont récupérées les données de l’IMU, qui restent en interne pour ne pas surcharger la liaison wifi avec HA.
Un peu de filtrage avec « exponential_moving_average » pour éviter les actions des vérins sans cesse à la moindre vibration.

sensor:
  - platform: template
    name: action verin roulis
    id: sensor_number_roulis

  - platform: template
    name: action verin tangage
    id: sensor_number_tangage

  - platform: mpu6050
    address: 0x68
    accel_x:
      id: accel_x
      name: "IMU Accel X"
      internal: True
      filters:
        - offset: $offset_accX
        #- timeout: 1s
        - clamp:
            min_value: -9.81
            max_value: 9.81
        - exponential_moving_average:
            alpha: 0.20
            send_every: 2
    accel_y:
      id: accel_y
      name: "IMU Accel Y"
      internal: True
      filters:
        - offset: $offset_accY
        #- timeout: 1s
        - clamp:
            min_value: -9.81
            max_value: 9.81
        - exponential_moving_average:
            alpha: 0.20
            send_every: 2
    accel_z:
      id: accel_z
      name: "IMU Accel z"
      internal: True
      filters:
        - multiply: -1.0
        - offset: $offset_accZ
        - timeout: 1s
        - clamp:
            min_value: 0.01 #IMU à l'envers, tombé
            max_value: 9.81
            ignore_out_of_range: False
        - exponential_moving_average:
            alpha: 0.20
            send_every: 2
    temperature:
      name: "MPU6050 Temperature"
      id: MPU_temp
      internal: True
    update_interval: $boucle_asservissement

Le calcul des angles des panneaux, par rapport à l’horizontal.
Si l’angle calculé est supérieur à 60, -60, 35, -53 (propre à mon installation), alors je passe tout de suite à mode Manuel, qui inhibe l’asservissement et donc le pilotage des vérins.
C’est valeurs correspondent à mes butées, amplitudes maximales.
Par exemple, l’IMU tombe …

  - platform: template
    id: PV_roulis
    name: PV roulis
    internal: True
    unit_of_measurement: °
    update_interval: $boucle_asservissement
    lambda: return atan2(id(accel_y).state, id(accel_z).state)*180/$PI;
    filters:
      - offset : $offset_roulis
    on_value_range: #si calcul d'un angle supérieur au réalisable mécanique, alors problème, arrêt de l'asservissement
      - above: 60.0 
        then:
          - switch.turn_on: manual_mode
      - below: -60.0
        then:
          - switch.turn_on: manual_mode
            
  - platform: template
    id: PV_tangage
    name: PV tangage
    internal: True
    unit_of_measurement: °
    update_interval: $boucle_asservissement
    lambda: return atan2(id(accel_x).state, id(accel_z).state)*180/$PI;
    filters:
      - offset : $offset_tangage
    on_value_range: #si calcul d'un angle supérieur au réalisable mécanique, alors problème, arrêt de l'asservissement
      - above: 35.0
        then:
          - switch.turn_on: manual_mode
      - below: -53.0
        then:
          - switch.turn_on: manual_mode

Une copie des valeurs de l’IMU, grandement ralenties, qui remontent dans HA.

  - platform: template
    name: PV roulis
    unit_of_measurement: °
    accuracy_decimals: 1
    update_interval: 10.20s #Mettre plus de temps une fois en service
    lambda: return id(PV_roulis).state;
    

  - platform: template
    name: PV tangage
    unit_of_measurement: °
    accuracy_decimals: 1
    update_interval: 10s #Mettre plus de temps une fois en service
    lambda: return id(PV_tangage).state;

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

Simple calcule, soustraction de l’angle du soleil et des panneaux, qui enclenche le script d’asservissement si le mode Manuel n’est pas ON.


  - platform: template
    id: OffsetRoll
    name: Offset Roulis
    internal: True
    update_interval: $boucle_asservissement
    lambda: return id(PV_roulis).state - id(sunRoulis).state;
    on_value: 
      - if:
          condition:
            - switch.is_off: manual_mode #vérification du mode manuel, si ON alors pas de pilotage
          then:
            - lambda: id(RollPilot)->execute(x);

  - platform: template
    id: OffsetPitch
    name: Offset Tangage
    internal: True
    update_interval: $boucle_asservissement
    lambda: return id(PV_tangage).state - id(sunTangage).state;
    on_value: 
      - if:
          condition:
            - switch.is_off: manual_mode #vérification du mode manuel, si ON alors pas de pilotage
          then:
            - lambda: id(PitchPilot)->execute(x);

Récupération des attitudes du soleil.
Pour le moment, j’ai commenté le Lambda, car je me suis aperçu que ça me générait des instabilités, mais j’aimerais le remettre dès que possible.

  - platform: sun
    name: Sun Elevation
    id: sunElevation
    type: elevation
    internal: True
    filters: 
#      - lambda: |-
#          if(isnan(x)){return {};}
#          else{return x;}
      - clamp:
          min_value: 05
          max_value: 89
          ignore_out_of_range: False
      
  - platform: sun
    name: Sun Azimuth
    id: sunAzimuth
    type: azimuth
    internal: True
    filters: 
#      - lambda: |-
#          if(isnan(x)){return {};}
#          else{return x;}
      - offset: $offset_azimuth
      - clamp:
          min_value: 60
          max_value: 310
          ignore_out_of_range: False

Ici, calcule afin de mettre la position du soleil dans le même repère que l’IMU afin de pouvoir comparer les valeurs.
Puis je mets les bornes logicielle, légèrement inférieures aux bornes physiques (amplitudes).
Si le soleil est en dehors, les panneaux sont asservies mais en butée, car « ignore_out_of_range: False »

  - 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
    name: sunRoulis
    unit_of_measurement: °
    internal: False
    update_interval: 10.1s
    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);
    filters: 
      - clamp:
          min_value: -52
          max_value: 52
          ignore_out_of_range: False

  - platform: template
    id: sunTangage
    name: sunTangage
    unit_of_measurement: °
    internal: False
    update_interval: 10.15s
    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;
    filters: 
      - clamp:
          min_value: -50
          max_value: 30
          ignore_out_of_range: False

Déclaration des deux switch pour le mode Manuel, Nuit/Tempête.
Au passage en mode Nuit/Tempête, je force la position du soleil vers où je veux que mes panneaux s’orientent. Je m’asservies sur un faux soleil.

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

  - 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

Enfin, la classe SUN, qui permet de récupérer les attitudes du soleil là où tu as installé le tracker.

time:
  - platform: homeassistant

sun:
  latitude: 4x.yyyyy
  longitude: 2.yyyyyy
  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
      - number.to_min: number_roulis # RàZ le soir
      - number.to_min: number_tangage # RàZ le soir
1 « J'aime »