Contents
Intro
Dans un article précédent, je décrivais une deuxieme version de routeur PV développé sous ESP Home donc complètement intégré à HA.
Dans cette version le programme a été totalement refondu et le principe de régulation inédit.
Vous pouvez que aussi consulter cet article décrivant une première version de routeur solaire, vous y trouverez le matériel utilisé.
Pourquoi un Routeur Solaire ?
Depuis l’installation de mes panneaux photovoltaïques, je cherchais une solution pour maximiser l’utilisation de l’énergie produite, plutôt que de l’injecter dans le réseau à un tarif dérisoire (environ 0,10 €/kWh en France). Mon objectif : rediriger le surplus d’énergie vers mon chauffe-eau électrique pour chauffer l’eau gratuitement. C’est ainsi que j’ai conçu un routeur solaire photovoltaïque basé sur ESPHome, un ESP32, et un gradateur AC, qui ajuste précisément la puissance envoyée à l’ECS (eau chaude sanitaire). Dans cet article, je vous détaille la configuration de mon système solaire, le fonctionnement du programme ESPHome, les quatre modes de régulation, le calcul de la puissance disponible, et comment l’ECS est alimenté en amont sur le grid de l’onduleur.
Un routeur solaire est un dispositif intelligent qui détecte l’excédent d’énergie produit par vos panneaux photovoltaïques et le redirige vers un appareil consommateur, comme un chauffe-eau, pour optimiser l’autoconsommation. Avec des tarifs de rachat d’électricité solaire peu attractifs, utiliser directement l’énergie produite est bien plus rentable.
Mon routeur solaire, surnommé esp176_routeur, utilise un ESP32 programmé avec ESPHome pour :
- Monitorer la production solaire, la consommation domestique, et l’état des batteries via MQTT.
- Réguler la puissance envoyée au chauffe-eau en fonction du surplus disponible.
- Garantir la sécurité avec des contrôles de température, de production minimale, et d’état de charge.
- S’intégrer à Home Assistant pour un suivi en temps réel, des réglages personnalisés, et des notifications Telegram.
Configuration Solaire
Mon installation photovoltaïque est conçue pour couvrir une partie de ma consommation domestique tout en stockant l’énergie dans des batteries pour une utilisation ultérieure. Voici les détails :
- Panneaux solaires : Puissance crête totale de 7.2 kWc.
- Onduleur : Un Victron MultiPlus II 48/5000, qui gère la conversion DC/AC, la charge des batteries, et l’injection/soutirage sur le réseau. L’onduleur est connecté à un Cerbo GX pour le monitoring et la publication des données via MQTT.
- Batteries : D’une capacité totale de 12 kWh, elles permettent de stocker l’excédent pour les périodes sans soleil.
- Chauffe-eau (ECS) : Un chauffe-eau électrique de 300 L avec une résistance de 3 kW, alimenté en amont sur le grid de l’onduleur (côté réseau AC, avant l’onduleur). Cela signifie que l’ECS consomme soit l’énergie solaire excédentaire, soit l’énergie du réseau si nécessaire, sous le contrôle du routeur.
Le Cerbo GX publie les données de l’onduleur (production, consommation, état des batteries) sur un broker MQTT, que l’ESP32 récupère pour calculer la puissance à injecter dans l’ECS.
Matériel du Routeur Solaire
Le routeur est construit autour d’un ESP32 DevKit, choisi pour sa puissance, ses multiples GPIO, et sa compatibilité avec ESPHome. Voici les composants principaux :
- Gradateur AC (dimmer) : Piloté via les broches GPIO33 (gate) et GPIO34 (zero-cross), il ajuste la puissance envoyée à l’ECS de 0 à 3 kW.
- Sonde de température Dallas DS18B20 : Connectée à GPIO27, elle mesure la température du radiateur pour éviter la surchauffe.
- Écran LCD 20×4 (PCF8574) : Relié via I2C (GPIO21/SDA, GPIO22/SCL), il affiche la puissance réseau, la sortie triac, la température, et le mode.
- Relais : Connecté à GPIO5, il s’active en cas de surplus important pour alimenter un autre appareil (par exemple, une pompe à chaleur).
- LEDs : Une LED jaune (GPIO26) indique l’état du routeur, une LED rouge (GPIO25) signale une injection réseau.
- Alimentation 5V : Alimente l’ESP32 et les composants.
Le tout est installé dans un boîtier DIN dans mon tableau électrique, avec une connexion Wi-Fi pour l’intégration à Home Assistant.
Programme ESPHome : Une Régulation Intelligente
Le cœur du routeur est un programme ESPHome qui gère la régulation, l’affichage, les logs, et l’intégration domotique. Voici une explication détaillée des éléments clés.
Les Quatre Modes de Fonctionnement
Le routeur propose quatre modes, sélectionnables via un sélecteur dans Home Assistant (select.esp176_mode_fonctionnement_routeur
) :
- Mode Auto :
- Description : Le routeur régule automatiquement la puissance envoyée à l’ECS en fonction du surplus solaire, tout en respectant des conditions de sécurité :
- Production solaire supérieure au seuil (
seuil_prod
, par exemple 100 W). - État de charge des batteries (SOC) supérieur au seuil (
seuil_soc
, par exemple 50 %). - Température du radiateur inférieure au maximum (
tmax
, par exemple 75 °C). - Routeur validé (
switch.esp176_valid_routeur
activé).
- Production solaire supérieure au seuil (
- Comportement : Si toutes les conditions sont remplies, le script
regulation_interpolation
calcule la puissance disponible (p_dispo
) et ajuste la sortie du triac (striac
) via le gradateur. Sinon, le triac est désactivé (striac = 0
).
- Description : Le routeur régule automatiquement la puissance envoyée à l’ECS en fonction du surplus solaire, tout en respectant des conditions de sécurité :
- Mode Manuel :
- Description : Permet de fixer manuellement la sortie du triac (de 0 à 100 %) via un réglage dans Home Assistant (
number.esp176_consigne_triac_en_manu
). - Comportement : Le triac prend la valeur définie, indépendamment de la production ou de l’état des batteries. Utile pour des tests ou pour forcer une chauffe. Le routeur reste sécurisé par la validation (
validrouteur
).
- Description : Permet de fixer manuellement la sortie du triac (de 0 à 100 %) via un réglage dans Home Assistant (
- Mode Arrêt :
- Description : Désactive complètement le routeur.
- Comportement : Le triac est mis à 0, le gradateur est éteint, et aucune puissance n’est envoyée à l’ECS. Le mode est publié comme « OFF » (
text_sensor.esp176_mode_regulation
).
- Mode Étalonnage :
- Description : Utilisé pour établir une table de correspondance entre la sortie du triac (0 à 100 %) et la puissance consommée par l’ECS (
puecs
). - Comportement : Le script
etalonnage_striac
incrémente la sortie du triac par pas de 1 % toutes les 10 secondes, mesure la puissance ECS, et enregistre les données via l’intégration « files » de HA. Ces données alimenteront la tablep_dispo_table
pour une régulation précise.
- Description : Utilisé pour établir une table de correspondance entre la sortie du triac (0 à 100 %) et la puissance consommée par l’ECS (
Calcul de la Puissance Disponible
Le calcul de la puissance disponible (p_dispo
) dépend de l’état de charge des batteries, déterminé par l’état du bus VE (etatbus_ve
, publié via MQTT) :
- Mode Bulk (etatbus_ve = 3) :
- Les batteries sont en charge rapide, et une partie du surplus doit leur être réservée.
- Formule :
p_dispo = pu_prod - conso_maison - res_pubatt
pu_prod
: Puissance produite par les panneaux solaires (par exemple, 2000 W).conso_maison
: Consommation domestique (par exemple, 800 W).res_pubatt
: Réserve pour les batteries, configurable dans Home Assistant (number.esp176_reserve_charge_batteries
, par exemple 500 W).
- Exemple : Si
pu_prod = 2000 W
,conso_maison = 800 W
,res_pubatt = 500 W
, alorsp_dispo = 2000 - 800 - 500 = 700 W
. - La réserve (
res_pubatt
) est publiée comme consigne de charge (sensor.esp176_cons_batt_en_cours
).
- Mode Absorption (etatbus_ve = 4) ou Float (etatbus_ve = 5) :
- Les batteries sont presque pleines ou pleines, et la puissance des batteries (
pu_batteries
) peut être négative (décharge) ou positive (charge lente). - Formule :
p_dispo = pu_prod - conso_maison + pu_batteries
pu_batteries
: Puissance des batteries, négative en décharge (par exemple, -200 W si les batteries soutiennent la maison).
- Exemple : Si
pu_prod = 2000 W
,conso_maison = 800 W
,pu_batteries = -200 W
, alorsp_dispo = 2000 - 800 + (-200) = 1000 W
. - La puissance des batteries (négative) est publiée comme
sensor.esp176_cons_batt_en_cours
.
- Les batteries sont presque pleines ou pleines, et la puissance des batteries (
- Autres états :
- Si
etatbus_ve
n’est ni 3, 4, ni 5, alorsp_dispo = 0
, et aucune puissance n’est envoyée à l’ECS.
- Si
La puissance disponible est limitée par la puissance maximale du triac (pmax
, par exemple 3000 W) et convertie en pourcentage de sortie triac (striac
) via une table de correspondance établie en mode Étalonnage. Une interpolation linéaire entre les valeurs de la table (p_dispo_table
) garantit une régulation précise.
Sécurité et Contrôles
Le routeur inclut plusieurs sécurités :
- Température : La sonde Dallas surveille la température du radiateur. Si elle dépasse
tmax
(par exemple, 75 °C), le triac est désactivé (temperatureok = false
). - Production minimale : Le routeur ne s’active que si
pu_prod > seuil_prod
(par exemple, 100 W). - État de charge (SOC) : Un seuil minimal (
seuil_soc
, par exemple 50 %) avec une hystérésis de 2 % protège les batteries. - Relais de surproduction : Si le triac est à 90 % et que l’ECS consomme moins de 100 W pendant 5 minutes, un relais s’active pour alimenter un autre appareil.
- Validation : Le routeur ne fonctionne que si
validrouteur
est activé.
Affichage LCD
Un écran LCD 20×4 affiche :
- Ligne 1 : Puissance réseau (
pureseau1
) et ECS (puecs
). - Ligne 2 : Sortie triac (
striac
) et état du routeur (validrouteur
: OK/NOK). - Ligne 3 : Température du radiateur (
temp_triac
) et état de la température (temperatureok
: OK/NOK). - Ligne 4 : Mode de fonctionnement (
Auto
,Manu
,Arret
,Etalonnage
).
Intégration Home Assistant
Les entités sont exposées dans Home Assistant via l’API ESPHome :
- Capteurs :
sensor.esp176_pu_disponible
,sensor.esp176_sortie_triac
,sensor.esp176_pu_reseau
. - Sélecteurs :
select.esp176_mode_fonctionnement_routeur
. - Nombres :
number.esp176_puissance_max_triac
,number.esp176_seuil_soc
. - Interrupteurs :
switch.esp176_valid_routeur
,switch.esp176_relais
.
Les logs d’étalonnage et les alertes sont envoyés via Telegram grâce à un script Home Assistant.
Configuration du fichier log:

Code du script appelé par ESPHome
Ce script ecrit les messages transmis par esphome dans un fichier, il peut servir à mémoriser des logs et/ou enregistrer les données en mod etalonnage:
alias: Envoyer un message log depuis ESP176
sequence:
- action: notify.send_message
target:
entity_id: notify.log_esp176
data:
message: "{{ message }}"
mode: single
fields:
message:
description: Le message à loguer
example: test depuis ESPHome
description: ""
Vue Synoptique de régulation

Code:
type: picture-elements
elements:
- entity: sensor.esp176_esp32_routeur_1r_mode_regulation
prefix: "Regul "
style:
background: null
color: white
font-size: 120%
left: 5%
top: 5%
transform: none
type: state-label
- entity: sensor.mp2_affichage_status_bus_ve
prefix: "Bus Ve= "
style:
background: null
color: white
font-size: 120%
left: 60%
top: 5%
transform: none
type: state-label
- entity: sensor.esp176_esp32_routeur_1r_pu_disponible
style:
background: none
color: white
font-size: 100%
left: 47%
top: 31%
transform: none
type: state-label
prefix: "P= "
- entity: sensor.esp176_esp32_routeur_1r_cons_batt_en_cours
prefix: "Bat= "
style:
background: null
color: white
font-size: 100%
left: 0%
top: 46%
transform: none
type: state-label
- entity: sensor.esp176_routeur_sortie_triac
prefix: ""
style:
background: null
color: white
font-size: 100%
left: 85%
top: 31%
transform: none
type: state-label
- entity: sensor.esp176_esp32_routeur_1r_p_ecs_jsymk
prefix: "ECS: "
type: state-label
style:
background: null
color: white
font-size: 100%
left: 47%
top: 48%
transform: none
- entity: sensor.mp2_prod_totale_mqtt
prefix: "Prod= "
type: state-label
style:
background: null
color: white
font-size: 100%
left: 0%
top: 26%
transform: none
- entity: sensor.mp2_conso_out1_mqtt
prefix: "Mais= "
type: state-label
style:
background: null
color: white
font-size: 100%
left: 0%
top: 36%
transform: none
- entity: sensor.esp176_routeur_temp_triac
prefix: "T° Triac= "
type: state-label
style:
background: null
color: white
font-size: 100%
left: 70%
top: 60%
transform: none
- entity: sensor.esp126_temp_ecs
prefix: "T° ECS= "
type: state-label
style:
background: null
color: white
font-size: 100%
left: 70%
top: 70%
transform: none
image: /local/images/pid_routeur_v4.png
Fichier image: https://github.com/remycrochon/home-assistant/tree/master/www/images
Alimentation de l’ECS
L’ECS est alimenté en amont sur le grid de l’onduleur, c’est-à-dire sur le réseau AC avant l’entrée de l’onduleur MultiPlus II. Cela signifie que :
- L’ECS peut consommer l’énergie solaire excédentaire gérée par le routeur.
- Si le surplus solaire est insuffisant, l’ECS tire l’énergie du réseau public.
- Le routeur ajuste la puissance via le gradateur pour minimiser le soutirage réseau, en s’appuyant sur les données de l’onduleur (publiées via MQTT).
Cette configuration garantit une intégration fluide avec l’onduleur, qui priorise l’alimentation de la maison et des batteries avant de laisser le surplus à l’ECS.
Installation et Mise en Route
- Assemblage : Connectez l’ESP32, le gradateur, la sonde Dallas, l’écran LCD, et le relais dans un boîtier DIN.
- Câblage : Branchez le gradateur à l’ECS, la sonde au radiateur, et le relais à un consommateur secondaire. Assurez-vous que l’ECS est connectée en amont de l’onduleur.
- Firmware : Flashez l’ESP32 avec le YAML ESPHome, en ajustant les secrets (Wi-Fi, MQTT) et l’IP statique (par ex 192.168.0.176).
- Configuration Home Assistant : Ajoutez l’appareil ESPHome et réglez les seuils (
pmax
,tmax
,seuil_soc
,seuil_prod
,res_pubatt
). - Étalonnage : Lancez le mode Étalonnage pour générer la table de correspondance, puis passez en mode Auto.
Résultats et Perspectives
Depuis son installation, le routeur a boosté mon autoconsommation. Par une belle journée, je redirige jusqu’à 3 kW de surplus vers l’ECS, réduisant l’injection réseau à presque zéro. Le relais de surproduction s’active occasionnellement pour alimenter ma pompe à chaleur.
Quelques idées d’amélioration :
- Ajouter un capteur de température d’eau pour arrêter le routeur quand l’ECS est chaude.
- Intégrer des prévisions météo pour anticiper la production.
- Ajouter un second gradateur pour un autre appareil.
Conclusion
Ce routeur solaire DIY, basé sur ESPHome et un ESP32, est une solution puissante pour maximiser l’autoconsommation photovoltaïque. Avec ses quatre modes de fonctionnement, ses sécurités, et son intégration à Home Assistant, il offre une flexibilité et une efficacité remarquables. Si vous avez une installation solaire avec un onduleur Victron et un chauffe-eau, ce projet est parfait pour vous !
Partagez vos retours ou vos propres projets dans les commentaires ou sur le forum Home Assistant. Le code YAML est disponible sur mon dépôt GitHub (esphome/esp176-routeur.yaml).
Annexes :
Codes ESPHome:
substitutions:
device_name: "esp176_routeur"
friendly_name: esp176
adress_ip: "192.168.0.176"
time_timezone: "Europe/Paris"
packages:
ph: !include pack_esp176/jsk.yaml
esphome:
name: ${device_name}
project:
name: "rem81.esp176-esp32-routeur"
version: "1.0.0"
on_boot:
priority: -100
# Force mode auto et tempok au demarrage
then:
- binary_sensor.template.publish:
id: temperatureok
state: ON
esp32:
board: esp32dev
framework:
type: arduino
wifi:
networks:
- ssid: !secret wifi_esp
password: !secret mdpwifi_esp
reboot_timeout: 5min
manual_ip:
static_ip: ${adress_ip}
gateway: 192.168.0.254
subnet: 255.255.255.0
dns1: 192.168.0.254
# Utilisez la LED bleue de l'appareil comme LED d'état, qui clignotera s'il y a des avertissements (lent) ou des erreurs (rapide)
status_led:
pin:
number: GPIO32 # led jaune
inverted: true
# Enable logging
logger:
baud_rate: 0
level: info
# modbus.component: INFO
# Enable Home Assistant API
api:
ota:
platform: esphome
web_server:
port: 80
version: 3
time:
- platform: sntp
id: sntp_time
timezone: Europe/Paris
servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
- 2.pool.ntp.org
# Protocole I2C
i2c:
sda: GPIO21
scl: GPIO22
scan: True
id: bus_a
frequency: 400kHz
# Mosquitto Proxmox
mqtt:
broker: 192.168.0.204
username: !secret mqtt_pve_name
password: !secret mqtt_pve_pw
#internal_mqtt_default: internal
globals:
- id: p_dispo_lisse
type: float
restore_value: yes
initial_value: '0'
- id: striac
type: float
restore_value: no
initial_value: '0.0'
# stocke temporairement le message à envoyer à telegram
- id: telegram_msg_buffer
type: std::string
restore_value: no
initial_value: '""'
# Sonde Temperature Dallas
one_wire:
- platform: gpio
pin: GPIO27 #
# Informations supplementaires sur le WIFI
#text_sensor:
# - platform: wifi_info
# ip_address:
# name: IP Address
# ssid:
# name: Connected SSID
# bssid:
# name: Connected BSSID
# mac_address:
# name: Mac Wifi Address
# scan_results:
# name: Latest Scan Results
# déclaration des modes de fonctionnement dans des "input select"
select:
- platform: template
name: "Mode_Fonctionnement_routeur"
optimistic: true
restore_value: true
options:
- Auto
- Manu
- Arret
- Etalonnage
id: _Mode_Fonctionnement_routeur
on_value:
then:
- if:
condition:
- lambda: 'return id(_Mode_Fonctionnement_routeur).state == "Etalonnage";'
then:
- script.execute: etalonnage_striac
- light.turn_off:
id: gradateur
- script.execute: calcul_injection
- logger.log:
format: "Mode Fonct Routeur --> %s"
args: [ 'id(_Mode_Fonctionnement_routeur).state.c_str()' ]
level: INFO
button:
- platform: template
name: "Bouton Declencheur"
binary_sensor:
#Etat de la connection
- platform: status
name: "Status"
- platform: template
name: "Temp Ok"
id: temperatureok
- platform: template
name: "Seuil Prod Ok"
id: seuil_prod_ok
- platform: template
name: "Seuil SOC Ok"
id: seuil_soc_ok
# Input Number
number:
# seuil SOC validation routeur
- platform: template
name: "Consigne Triac en manu"
id: ctriac_manu
optimistic: true
restore_value: true
mode: box
min_value: 0
max_value: 100
unit_of_measurement: "%"
step: 1
icon: mdi:arrow-collapse-vertical
# Max sortie triac
- platform: template
name: "Puissance Max Triac"
id: pmax
optimistic: true
restore_value: true
mode: box
min_value: 10
max_value: 3000
unit_of_measurement: "W"
step: 1
icon: mdi:arrow-collapse-vertical
# Seuil MAX temperature
- platform: template
name: "T Max"
id: tmax
optimistic: true
restore_value: true
mode: box
min_value: 0
max_value: 75
unit_of_measurement: "C°"
step: 0.1
icon: mdi:arrow-collapse-vertical
# Consigne Régul sur Puissance Batteries en mode Bulk
- platform: template
name: "Reserve Charge Batteries"
id: res_pubatt
optimistic: true
restore_value: true
mode: box
min_value: 0
max_value: 2500
unit_of_measurement: "W"
step: 1
icon: mdi:arrow-collapse-vertical
# seuil SOC validation routeur
- platform: template
name: "Seuil SOC"
id: seuil_soc
optimistic: true
restore_value: true
mode: box
min_value: 0
max_value: 100
unit_of_measurement: "%"
step: 1
icon: mdi:arrow-collapse-vertical
# seuil Production Photovoltaique de validation routeur
- platform: template
name: "Seuil Production Val Routeur"
id: seuil_prod
optimistic: true
restore_value: true
mode: box
min_value: 100
max_value: 3000
unit_of_measurement: "W"
step: 1
icon: mdi:arrow-collapse-vertical
sensor:
# Informations WI_FI
- platform: wifi_signal # Affiche le signal WiFi strength/RSSI en dB
name: "WiFi Signal dB"
update_interval: 60s
############### TEMPLATE ######################"
# Affichage dans HA et sur l'afficheur
# Puissance lue par le JSk- Négative en injection/Positive en soutirage
- platform: template
name: "Pu Reseau"
id: pureseau1
unit_of_measurement: "W"
state_class: "measurement"
accuracy_decimals: 0
# Sortie triac de 0à100%
- platform: template
name: "Sortie Triac"
id: afstriac
unit_of_measurement: "%"
state_class: "measurement"
accuracy_decimals: 2
# Pu disponible
- platform: template
name: "Pu Disponible"
id: afpdispo
unit_of_measurement: "W"
state_class: "measurement"
accuracy_decimals: 0
# Sensor Intermediaire pour synoptique
- platform: template
name: "Cons batt en Cours"
id: cons_batt_cours
state_class: "measurement"
unit_of_measurement: "W"
accuracy_decimals: 0
# Les MQTT sont déclarés dans le Node Red du Cerbo GX
# https://venus.local:1881/#flow/dbd727f16cbe7b5f
- platform: mqtt_subscribe
name: "Conso Maison"
id: conso_maison
topic: mp2/multiplus2/conso_out1
unit_of_measurement: "W"
state_class: "measurement"
accuracy_decimals: 2
filters:
- sliding_window_moving_average:
window_size: 10
send_every: 1
- platform: mqtt_subscribe
name: "Pu batterie"
id: pu_batteries
topic: mp2/batteries/puissance
unit_of_measurement: "W"
state_class: "measurement"
accuracy_decimals: 2
filters:
- sliding_window_moving_average:
window_size: 10
send_every: 1
- platform: mqtt_subscribe
name: "Pu Produite"
id: pu_prod
topic: mp2/multiplus2/prod_totale
unit_of_measurement: "W"
state_class: "measurement"
accuracy_decimals: 2
filters:
- sliding_window_moving_average:
window_size: 10
send_every: 1
- platform: mqtt_subscribe
name: "Soc"
id: soc
topic: mp2/batteries/soc
unit_of_measurement: "%"
state_class: "measurement"
accuracy_decimals: 2
filters:
- sliding_window_moving_average:
window_size: 10
send_every: 1
- platform: mqtt_subscribe
name: "Etat Bus VE"
id: etatbus_ve
topic: mp2/multiplus2/etatbusve
accuracy_decimals: 0
# lecture sensor Home Assistant
- platform: homeassistant
name: "Status Bus VE"
entity_id: sensor.mp2_status_bus_ve
id: statusbusve
- platform: homeassistant
name: "Tarif Num"
entity_id: sensor.linky_n_tarif
id: hc
# Sonde Temperature radiateur
- platform: dallas_temp
address: 0xeb012112e461b128
name: "Temp triac"
id: temp_triac
update_interval: 60s
filters:
- filter_out: NAN
# déclaration des "text_sensors"
text_sensor:
- platform: template
name: "Mode Regulation"
id: moderegul
switch:
- platform: gpio
name: "Relais"
pin: GPIO5
id: relais
- platform: template
name: "Valid Routeur"
id: validrouteur
optimistic: true
restore_mode: always_on
- platform: restart
name: "Restart"
output:
#LEDS --------------------------------------
- id: led_jaune
platform: gpio
pin: GPIO26 #GPIO32
- id: led_rouge
platform: gpio
pin: GPIO25
# Pilotage du Dimmer
- platform: ac_dimmer
id: ecs
gate_pin: GPIO33
method: leading
zero_cross_pin:
number: GPIO34
mode:
input: true
inverted: yes
min_power: 5%
light:
- platform: monochromatic
name: "STriac"
output: ecs
id: gradateur
default_transition_length: 50ms
# Affichage
display:
- platform: lcd_pcf8574
dimensions: 20x4
address: 0x27
update_interval: 20s # Plus espacé pour alléger la charge CPU
lambda: |-
char ligne0[21];
char ligne1[21];
char ligne2[21];
char ligne3[21];
snprintf(ligne0, sizeof(ligne0), "Pr=%0.0fW Pe=%0.0fW", id(pureseau1).state, id(puecs).state);
snprintf(ligne1, sizeof(ligne1), "Tr=%0.1f%% V:%s", id(striac), id(validrouteur).state ? "OK" : "NOK");
snprintf(ligne2, sizeof(ligne2), "Tp=%0.1fc E:%s", id(temp_triac), id(temperatureok).state ? "OK" : "NOK");
snprintf(ligne3, sizeof(ligne3), "Mode:%s", id(_Mode_Fonctionnement_routeur).state.c_str());
it.print(0, 0, ligne0);
it.print(0, 1, ligne1);
it.print(0, 2, ligne2);
it.print(0, 3, ligne3);
interval:
- interval: 1s
then:
- script.execute: calcul_injection
- interval: 5s
then:
- script.execute: etat_production
- script.execute: calcul_relais_surprod
########################################################################""
script:
# Calcul du niveau de puissance à injecter dans le triac pilotant l'ECS
# En Auto: Conditions Initales de Démarrage:
# Mode de Fct=Auto
# Seuil Prod Ok
# Seuil SOC Ok
# temp Triac Ok
# Routeur Validé
- id: calcul_injection
mode: single
then:
- lambda: |-
// Pu production > Seuil de production
id(seuil_prod_ok).publish_state(id(pu_prod).state > id(seuil_prod).state);
// # Seuil de SOC (avec hysteresis de 2 %)
if (id(soc).state >= id(seuil_soc).state) {
id(seuil_soc_ok).publish_state(true);
} else if (id(soc).state < (id(seuil_soc).state - 2)) {
id(seuil_soc_ok).publish_state(false);
}
// Surveille température triac
if (id(temp_triac).state < (id(tmax).state - 2)) {
id(temperatureok).publish_state(true);
} else if (id(temp_triac).state >= id(tmax).state) {
id(temperatureok).publish_state(false);
}
// Log de débug
// ESP_LOGI("regulp", "P Prod; %.0f Seuil: %.2f",id(pu_prod).state,id(seuil_prod).state);
# Si conditions non Ok alors RAZ du Triac
- if:
condition:
or:
# Cas 1 : Mode Arret ==> OFF
- lambda: 'return id(_Mode_Fonctionnement_routeur).state == "Arret";'
# Cas 2 : Mode auto avec conditions NOK => OFF
- and:
- lambda: 'return id(_Mode_Fonctionnement_routeur).state == "Auto";'
- or:
- switch.is_off: validrouteur
- binary_sensor.is_off: temperatureok
- binary_sensor.is_off: seuil_prod_ok
- binary_sensor.is_off: seuil_soc_ok
then:
- lambda: |-
id(striac) = 0;
id(moderegul).publish_state("OFF");
id(afpdispo).publish_state(0);
id(cons_batt_cours).publish_state(0);
- light.turn_off: gradateur
- logger.log:
format: "Régulation OFF - Mode: %s - Striac: %.1f"
args: ['id(moderegul).state.c_str()', 'id(striac)']
level: DEBUG
# Si toutes les conditions OK alors on calcule la S triac
- if:
condition:
and:
# Cas 3 : Mode auto + toutes les conditions OK => PID actif
- lambda: 'return id(_Mode_Fonctionnement_routeur).state == "Auto";'
- switch.is_on: validrouteur
- binary_sensor.is_on: seuil_prod_ok
- binary_sensor.is_on: temperatureok
- binary_sensor.is_on: seuil_soc_ok
then:
# Vers script de régulation
- script.execute: regulation_interpolation
- light.turn_on:
id: gradateur
brightness: !lambda |-
return id(striac) / 100;
# Mode Manuel
- if:
condition:
and:
- lambda: 'return id(_Mode_Fonctionnement_routeur).state == "Manu";'
- switch.is_on: validrouteur
then:
- lambda: |-
// Application de striac avec sécurité
id(striac) = id(ctriac_manu).state;
// Publication des états
id(afpdispo).publish_state(0);
id(moderegul).publish_state("Manu");
id(cons_batt_cours).publish_state(0);
- light.turn_on:
id: gradateur
brightness: !lambda |-
return id(striac) / 100;
# Affichage STriac
- lambda: |-
id(afstriac).publish_state( id(striac)) ;
########################################################################""
# Principe de la régulation:
# Si Bus Ve en Bulk:, on partage le surplus entre la batterie (cons_batt) et l'ECS
# P Dispo= Prod-Conso Maison-Reserve batteries
# Si Bus Ve en Absortion ou Float, cela signifie que la batterie est chargée, alors:
# P Dispo= Prod-Conso Maison-Pu batteries (Negative en décharge)
# Sinon:
# P Dispo = 0
# La sortie triac est calculée en fonction de la P_Dispo en recherchant sa valeur dans la table de correspondance établis en mode étalonnage
- id: regulation_interpolation
mode: single
then:
- lambda: |-
float p_dispo=0 ;
std::string regul;
// Table de correspondance striac vs p_dispo
const float striac_table[] = {
0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0,
11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0,
21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0,
31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, 40.0,
41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0,
51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, 60.0,
61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0,
71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, 80.0,
81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0,
91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, 100.0
};
const float p_dispo_table[] = {
5.0, 14.661659, 15.560880, 15.927020, 16.568291, 18.303759, 19.157148, 20.944599, 22.596100, 23.025951, 25.903030,
28.434729, 30.524710, 31.625759, 34.235725, 36.901440, 40.163738, 44.019341, 44.768658, 49.555248, 56.278118,
59.905346, 64.042496, 69.965248, 77.182022, 81.073914, 89.339218, 96.296753, 100.722771, 108.025780, 115.575485,
122.833633, 133.766602, 144.135086, 159.488968, 166.558502, 178.282349, 189.092865, 202.196732, 212.861603, 234.333084,
248.347321, 262.094543, 283.565796, 301.493713, 322.728455, 344.373596, 365.494080, 381.928162, 415.564850, 441.482819,
468.173431, 501.893982, 521.712341, 550.043701, 586.746399, 627.726074, 661.966309, 703.051208, 730.505554, 761.137329,
804.480469, 847.356140, 878.056946, 941.720215, 993.722656, 1061.685303, 1101.080322, 1135.102417, 1196.311035, 1233.961548,
1294.063599, 1352.521729, 1392.430054, 1460.270386, 1512.882202, 1609.747437, 1674.149170, 1696.517944, 1770.225342, 1833.700928,
1901.670288, 1970.877197, 2023.935547, 2094.105469, 2165.517334, 2248.448242, 2357.001465, 2456.213135, 2539.881348, 2570.754395,
2631.065918, 2691.585938, 2750.663086, 2817.201172, 2895.280762, 2941.024902, 3038.455322, 3106.765137, 3144.267822, 3183.569824
};
const int table_size = sizeof(striac_table) / sizeof(striac_table[0]);
// Sélection de Consigne et Mesure en fonction de la charge des batteries
if (id(etatbus_ve).state == 3) {
// Mode Bulk : régule sur la puissance batterie
// Calcul de la puissance disponible
p_dispo = id(pu_prod).state - id(conso_maison).state-id(res_pubatt).state;
p_dispo = (p_dispo < 0.0) ? 0.0 : p_dispo;
// Affichage sur Synoptique
id(cons_batt_cours).publish_state(id(res_pubatt).state);
regul= "Sur Pu batteries Bulk";
} else if ((id(etatbus_ve).state == 4) || (id(etatbus_ve).state == 5)){
// Sinon, on reste sur la puissance batterie en mode Absorsion (4) ou Float (5)
p_dispo = id(pu_prod).state - id(conso_maison).state+id(pu_batteries).state;
p_dispo = (p_dispo < 0.0) ? 0.0 : p_dispo;
// Affichage sur Synoptique
id(cons_batt_cours).publish_state(id(pu_batteries).state*-1);
regul= "Sur P Batt Absord/Float";
} else {
regul= "Pas de régulation";
id(cons_batt_cours).publish_state(0);
p_dispo = 0;
}
// limite la P du triac à P Max
p_dispo = constrain(p_dispo, 0.0, id(pmax).state);
// Recherche dans la table avec interpolation linéaire
float striac_f = 0.0;
if (p_dispo <= p_dispo_table[0]) {
striac_f = striac_table[0]; // Valeur minimale
} else if (p_dispo >= p_dispo_table[table_size - 1]) {
striac_f = striac_table[table_size - 1]; // Valeur maximale
} else {
// Interpolation linéaire
for (int i = 0; i < table_size - 1; i++) {
if (p_dispo >= p_dispo_table[i] && p_dispo <= p_dispo_table[i + 1]) {
float ratio = (p_dispo - p_dispo_table[i]) / (p_dispo_table[i + 1] - p_dispo_table[i]);
striac_f = striac_table[i] + ratio * (striac_table[i + 1] - striac_table[i]);
break;
}
}
}
// Application de striac avec sécurité
if (isnan(id(striac))) striac_f = 0.0;
id(striac) = constrain(striac_f, 0.0, 100.0); // Limite striac entre 0 et 100
// Publication des états
id(afpdispo).publish_state(p_dispo);
id(moderegul).publish_state(regul);
// Log de débogage
ESP_LOGI("regul", "p_dispo: %.2f, pu_prod: %.2f, conso_maison: %.2f, pu_batteries: %.2f,STriac: %.2f",
p_dispo, id(pu_prod).state, id(conso_maison).state, id(cons_batt_cours).state,id(striac));
########################################################################""
# Mode Etalonnage Increment S Traic toutes les 20 s pour laisser du temps à la puissance pour se stabiliser
- id: etalonnage_striac
mode: restart
then:
- lambda: |-
id(striac) = 0.0;
- while:
condition:
lambda: 'return id(striac) < 100.0;' # S'arrête après striac = 100
then:
- lambda: |-
id(striac) += 1.0; // Incrémente striac
ESP_LOGI("striac", "Valeur striac: %.2f", id(striac));
- light.turn_on:
id: gradateur
brightness: !lambda 'return id(striac) / 100.0;' # Normalise entre 0.0 et 1.0
- delay: 20s # Temporisation
- lambda: |-
ESP_LOGI("striac", "Valeur striac: %.2f Pu ECS %.0f", id(striac), id(puecs).state);
- script.execute: log_striac1
- lambda: |-
ESP_LOGI("striac", "Fin de l'étalonnage, striac = %.2f", id(striac));
########################################################################""
# Enregistre dans un fichier
- id: log_striac
mode: single
then:
- lambda: |-
std::string mess = "";
mess += std::to_string(id(striac)) + ",";
mess += std::to_string(id(puecs).state);
ESP_LOGI("fichier", "Message: %s", mess.c_str());
if (id(sntp_time).now().is_valid()) {
ESP_LOGI("fichier", "Temps valide: %s", id(sntp_time).now().strftime("%Y-%m-%d %H:%M:%S").c_str());
} else {
ESP_LOGI("fichier", "Temps non synchronisé");
}
id(_log_message).execute(mess);
- id: log_striac1
mode: single
then:
- lambda: |-
std::string mess = "";
mess += std::to_string(id(striac)) + ",";
mess += std::to_string(id(puecs).state);
ESP_LOGI("fichier", "Message: %s", mess.c_str());
id(_log_message).execute(mess); // Appelle le script _log_message avec le paramètre mess
########################################################################""
# ------------ Pilotage led
- id: etat_production
mode: single
then:
- if:
condition:
sensor.in_range:
id: pureseau1
below: -50
then:
- output.turn_on: led_rouge
else:
- output.turn_off: led_rouge
- if:
condition:
switch.is_on: validrouteur
then:
- output.turn_on: led_jaune
else:
- output.turn_off: led_jaune
########################################################################""
- id: calcul_relais_surprod
mode: single
then:
# Si sortie triac > pmax-5%, ce qui signifie que le triac est au max sans effet, pendant plus de 60s
# alors on active le relais
# si triac <= 0 alors on desactive le relais
- if:
condition:
- lambda: 'return (id(striac)>=90 && id(puecs).state<100);'
then:
- delay: 300s
- switch.turn_on: relais
- logger.log: "Relais Activé"
- if:
condition:
- lambda: 'return id(puecs).state >= 100;'
then:
- switch.turn_off: relais
- logger.log: "Relais Désactivé"
- id: _log_message
parameters:
mess1: std::string # Type explicite pour ESPHome
then:
- lambda: |-
std::string mess = mess1; // Utilise directement mess1 sans horodatage
id(telegram_msg_buffer) = mess;
ESP_LOGI("log_message", "Telegram buffer: %s", id(telegram_msg_buffer).c_str());
- homeassistant.service:
service: script.envoyer_un_message_log_depuis_esp176
data:
message: !lambda 'return id(telegram_msg_buffer).c_str();'
Code fichier JSK
# Protocole du JSK
uart:
id: mod_bus
tx_pin: 17
rx_pin: 16
baud_rate: 38400
stop_bits: 1
# debug:
# direction: BOTH
# dummy_receiver: false
# after:
# timeout: 150ms
# sequence:
# - lambda: |-
# UARTDebug::log_string(direction, bytes);
modbus:
#flow_control_pin: 5
#send_wait_time: 200ms
id: modbus1
modbus_controller:
- id: jsymk
## the Modbus device addr
address: 0x1
modbus_id: modbus1
update_interval: 0.75s
command_throttle: 50ms
# setup_priority: -10
sensor:
# tension de l'alimentation
- platform: modbus_controller
modbus_controller_id: jsymk
id: Tension
#name: "Tension JSYMK"
address: 0x0048
unit_of_measurement: "V"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
# Intensité traversant le tore
- platform: modbus_controller
modbus_controller_id: jsymk
id: Itore
name: "I_ECS JSYMK"
address: 0x0049
unit_of_measurement: "A"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
state_class: measurement
# Puissance traversant le tore
- platform: modbus_controller
modbus_controller_id: jsymk
id: puecs
name: "P_ECS JSYMK"
address: 0x004A
unit_of_measurement: "W"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
- sliding_window_moving_average:
window_size: 10
send_every: 1
register_count: 1
response_size: 4
state_class: measurement
# Energie lue dans le tore
- platform: modbus_controller
modbus_controller_id: jsymk
id: energietore
name: "Energie ECS JSYMK"
address: 0x004B
unit_of_measurement: "kWh"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
state_class: total
# Energie lue dans le tore
- platform: modbus_controller
modbus_controller_id: jsymk
id: fptore
#name: "FP Tore JSYMK"
address: 0x004C
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
# Energie NEG lue dans le tore
- platform: modbus_controller
modbus_controller_id: jsymk
id: energietoren
name: "Energie ECS Neg JSYMK"
address: 0x004D
unit_of_measurement: "kWh"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
state_class: total
# Sens du courant dans la pince
- platform: modbus_controller
modbus_controller_id: jsymk
id: senspince
#name: "Sens_Pince JSYMK"
address: 0x004E
register_type: holding
value_type: U_DWORD
bitmask: 0X00010000
filters:
- multiply: 1
register_count: 1
response_size: 4
# Sens du courant dans le tore
- platform: modbus_controller
modbus_controller_id: jsymk
id: senstor
#name: "Sens_Tore JSYMK"
address: 0x004E
register_type: holding
value_type: U_DWORD
accuracy_decimals: 0
bitmask: 0X01000000
filters:
- multiply: 1
register_count: 1
response_size: 4
# Fréquence de l'alimentation
- platform: modbus_controller
modbus_controller_id: jsymk
id: frequence
#name: "Frequence JSYMK"
address: 0x004F
unit_of_measurement: "hz"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.01
register_count: 1
response_size: 4
# tension de l'alimentation
- platform: modbus_controller
modbus_controller_id: jsymk
id: Tension2
#name: "U_Reseau JSYMK"
address: 0x0050
unit_of_measurement: "V"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
# Intensité lue dans la pince
- platform: modbus_controller
modbus_controller_id: jsymk
id: Ireseau
#name: "I_Reseau JSYMK"
address: 0x0051
unit_of_measurement: "A"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
# puissance lue dans la pince
- platform: modbus_controller
modbus_controller_id: jsymk
id: pureseau
#name: "P_Reseau JSYMK"
address: 0x0052
unit_of_measurement: "W"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
on_value:
then:
- lambda: |-
if ( id(senspince).state == 1 ) {
id(pureseau1).publish_state( id(pureseau).state *-1);
} else {
id(pureseau1).publish_state( id(pureseau).state );
}
# Energie lue dans la pince
- platform: modbus_controller
modbus_controller_id: jsymk
id: energiepince
#name: "Energie Reseau JSYMK"
address: 0x0053
unit_of_measurement: "kWh"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
# Energie lue dans le tore
- platform: modbus_controller
modbus_controller_id: jsymk
id: fppince
#name: "FP Pince JSYMK"
address: 0x0054
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
# Energie NEG lue dans le tore
- platform: modbus_controller
modbus_controller_id: jsymk
id: energienegpince
#name: "Energie ECS Neg JSYMK"
address: 0x0055
unit_of_measurement: "kWh"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4