Sommaire
Attention : ce projet n'est pas destiné à tout le monde.
Si vous n'êtes pas confiant en vos compétences d'électricité, de soudure, n'installez pas ce dispositif.
En effet, les niveaux de puissance mis en jeu par ce dispositif sont loin d'être négligeables. Risque de blessures graves ou pire.
Je dégage toute responsabilité, tout comme le fera HACF, en cas de problème rencontré, quel qu'il soit.
Préambule
Venant de faire installer quelques panneaux solaires (2.8 kW crête), j'étais à la recherche d'une solution de routeur solaire et surtout compatible Home Assistant en DIY. J'ai donc décidé de créer mon propre routeur solaire : création d'un circuit imprimé et soudage des composants, conception et impression en 3D du boitier. Je vous présente ici mon projet.
Un routeur solaire, qu'est-ce donc ?
Le routeur solaire permet de maximiser l'utilisation de l'électricité produite par les panneaux solaires en utilisant l'électricité excédentaire pour chauffer l'eau dans un chauffe-eau électrique. Il contribue ainsi à réduire la consommation d'énergie provenant du réseau électrique traditionnel.
En d'autres termes, j'essaye d'injecter de manière automatique un maximum d'énergie issue de ma production solaire dans mon eau chaude sanitaire, tout en minimisant l'apport d'énergie issue du réseau ENEDIS.
Comment ça marche ?
Le cœur du système est un micro contrôleur de type ESP32 que l'on programmera avec ESPHome. Ainsi, le système sera compatible avec Home Assistant.
Le microcontrôleur prend en entrée la puissance consommée ou réinjectée par la maison, et en sortie pilotera en fonction le chauffe-eau :
- Si la puissance est positive, alors on consomme ( la production solaire est insuffisante ). On n'alimente pas le chauffe-eau.
- Si elle est négative, on injecte de l'énergie dans le circuit EDF. On va faire en sorte d'alimenter le chauffe-eau.
Le système intègre un module de mesure avec un tore pour mesurer la puissance sur la ligne d'alimentation de la maison, et un second pour mesurer ce qui est injecté dans le chauffe-eau.
Le module de sortie qui aliment le chauffe-eau dispose d'une sécurité : une sonde mesure la température du composant principal (triac avec radiateur) et si elle est trop élevé, le système s'arrête.
Pour finir il y a un petit afficheur de contrôle.
Nous verrons plus en détail dans les chapitres qui suivent le fonctionnement de chaque composant.
Pourquoi le "et plus encore" ? Mesurer c'est savoir.
Le module peut faire plus que le routage solaire. J'ai rajouté à ce projet deux autres fonctions :
- La récupération des données de la télé-information du compteur Linky (TIC). (testé uniquement en mode historique)
- La capacité de connecter 4 sondes de mesure de courant supplémentaires non intrusives, ce qui permet de mesurer les postes consommateurs de ma maison.
Typiquement :- Chauffage (pompe à chaleur, convecteurs...),
- Plaque de cuisson,
- four,
- Lave-vaisselle,
A quel appareil est destiné le routeur ?
Le routeur ne peut pas piloter tous les appareils. Il est impératif de lire les restrictions qui suivent.
- Le routeur n'est utilisable que sur des charges exclusivement résistives, donc adieu moteur, pompe. Dans ce cas-là, il est impératif d'utiliser un dispositif adapté.
- Le routeur n'est pas utilisable pour les chargeurs de batterie ou de véhicule électrique. Il y a des équipements spécifiques dont c'est la fonction.
- Enfin, tous les chauffe-eaux ne sont pas compatibles. Seuls les chauffe-eaux à thermostat mécanique sont compatibles avec cette solution.
Concernant les chauffe-eaux :
- les chauffe-eaux thermodynamiques ne sont pas compatibles avec cette solution
- les chauffe-eaux à régulation électronique ne sont pas compatibles avec cette solution

- seul et seulement les chauffe-eaux eau à thermostat mécanique sont compatibles avec un routeur solaire.

Mes sources
Je me suis inspiré pour ce projet de différentes réalisations. J'ai identifié quelques projets dont la plupart sont décrits sur le forum photovoltaïque.
Un autre projet est celui de F1ATB : routeur photovoltaïque simple à réaliser. Il nous réalise quasiment tous les 6 mois une évolution de sa solution ultra-performante et universelle Je vous laisse potasser son site très détaillé. Un excellent pédagogue. Merci aussi à lui pour sa capacité à nous faire comprendre les choses.
Ensuite, au cours de l'année 2023 je suis tombé sur une solution à 100% ESPHOME. C'est le routeur de REM81 : PV-Routeur Solaire ESP Home, alias @Remy_Crochon sur le forum et dont je me suis très largement inspiré.
Ma Configuration Actuelle
Afin d'expliquer un peu mon besoin et mes attentes je dois vous présenter ma configuration actuelle :
- Un contrat EDF HC/HP en monophasé, avec 2 périodes de tarif HC (un le jour et un la nuit).
- Compteur Linky en mode historique (important car le firmware de l'ESP va évoluer si vous êtes en mode standard, je n'ai pas testé)
- Un chauffe-eau de 3.3 kW.
- 2.8 kW crête de photovoltaïque orienté plein sud en région toulousaine.
- Du tout électrique à la maison.
- Un talon de 250 à 300 VA.
Comme vous pouvez le constater, au mieux, mon installation photovoltaïque peut produire 2.2 kW voir 2.5 kW, donc insuffisant pour alimenter exclusivement le cumulus sans faire appel à un complément EDF si je n'utilise pas de routeur.
J'ai une consommation quotidienne moyenne de 4 kWh d'eau chaude. Dont presque la moitié pour les pertes (quand je suis absent j'ai constaté un complément quotidien de 1.8 à 2 kWh) 👀 (inadmissible, oui je dois aussi travailler là-dessus).
Le postulat de base :
- Effacer le talon de la maison,
- "Charger" au mieux le chauffe-eau même si l'apport solaire est insuffisant de manière instantanée.
Une réalisation fiabilisée
Le petit hic de beaucoup des solutions DIY, c'est que l'assemblage des différents éléments n'est pas vraiment à mon goût. En effet, raccorder les différents sous-ensembles par des fils Dupond, c'est bien pour de l'expérimentation ou du prototype. Mais dès qu'il y a de la puissance ou du secteur je suis plus à l'aise avec une solution plus intégrée.
Si vous voulez voir des expériences électriques foireuses, allez sur la chaine YouTube d'ElectroBOOM.
Le but de ce sujet est de passer de ça :

À ça :

Je me suis donc mis en tête de concevoir un circuit imprimé afin de fiabiliser un peu plus tout ça sans trop avoir un plat de spaghettis dans une boite.
L'électronique
Ce projet est constitué de plusieurs "modules" ou fonctions.
- le module TIC,
- le module mesure de courant.
- le module d'acquisition dédié au routeur solaire avec le cerveau du projet
- l'affichage,
- le module dimmer (variateur de puissance),
Schéma du module télé-information
Ce circuit est utilisé pour la récupération de la télé-information. Attention mon installation étant en mode historique, je vous laisse adapter le code si vous êtes en mode standard. je n'ai pas testé si l'esp est capable de prendre en compte toutes ces fonctions en mode standard.
il y a ce dépôt qui donne je crois tout un tas de configurations ( mono, tri phasé, historique, standard), sinon vous avez tout un tas de sujets sur le forum HACF.
Le schéma est celui qui est largement diffusé sur le forum. Je voudrais aussi remercier Charles Hallard qui est à l'origine de ce schéma.
Merci mille fois Charles.
Je vous invite aussi à voir l'article d'Argonaute, pour des usages complémentaires (Dashboard énergie par exemple).

Schéma du module d'acquisition
Le schéma suivant présente un module d'acquisition permettant de mesurer le courant sur 4 circuits différents.
Pour ces 4 circuits de courant, j'ai fait appel à des (ADC) convertisseurs Analogiques Numérique ADS 1115 I²C sur lesquels je branche des pinces ampèremétriques SCT013-30. Cela me permet de mesurer jusqu'à 30 ampères par canal.
Bien que vous puissiez choisir d'autres modèles de sondes de courant (d'autres plages de mesures), assurez-vous de prendre des sondes renvoyant une tension et non pas un courant. Les sondes 100A/50mA ne sont pas compatibles car elles renvoient un courant. D'autre part si vous faites le choix de changer de sonde de courant, le code est à adapter ; ce sera expliqué dans le détail du code.

Les ADS 1115 ont la particularité de pouvoir fonctionner en mode différentiel, ce qui me permet de m'affranchir de créer une masse virtuelle à base de condensateurs, mais surtout élimine drastiquement les parasites (voir ce post sur le forum). ce qui est pratique dans un tableau électrique.
Afin de limiter l'effet d'antenne, les prises jack sont des modèles avec mise à la masse lorsque le canal de mesure n'est pas utilisé (pas de sonde insérée).

Schéma du module d'acquisition pour la partie Routage
le système de mesure, de commande et de visualisation est basé sur :
- Un module d'acquisition JSY-MK-194,
- Un ESP32,
- Un écran LCD 20*4 ,
- Un système de mesure de température à base de DS18b20
- Quelques petits éléments complémentaires (alimentation, led ...).

Schéma du variateur de puissance
Le circuit du variateur de puissance (ou "dimmer") est basé sur un variateur à triac tel que ceux utilisés pour les dimmers de chez robotdyn. Attention comme précisé au début de l'article cette variante ne peut être utilisée que pour des charges résistives uniquement (en effet la partie snubber couple R9 C3 a été supprimée).

Réalisation du circuit
Le circuit imprimé
Il nous faut maintenant réaliser le circuit imprimé. J'ai choisi un format Europe (100 * 160) normalisé.
Je n'ai plus les yeux ni la patience, encore moins le matériel pour du composant en CMS (composant en montage en surface) j'utilise donc des composants traversants (truehole).
Voici le circuit :

Vous pouvez retrouver le circuit sur mon github : Fichier GERBER
Je recommande de passer par JLCPCB pour la réalisation du circuit imprimé, mais il y a d'autres fournisseurs qui permettent la réalisation de circuit imprimé par exemple : PCBWAY

Idem pour les composants standards qui s'achètent par lot.
Pour information, je n'ai pas prévu de faire de kits. Ni HACF
Les composants
La part list est elle-aussi disponible sur mon github.
Attention :
- Les résistances R48 et R410 (valeur 120K) doivent être IMPERATIVEMENT des résistances de 1W minimum.
- Le module alimentation est parfois cloné et le brochage est différent (espacement différent sur les broches du secteur) le hi-link est conforme à mon circuit imprimé (les modules de AZ DELIVERY ne le sont pas) regardez bien la différence d'espacement entre les pins du haut et celles du bas. C'est seulement ce modèle qui est compatible.

- Les prises jack femelle doivent avoir un contact à la masse quand non utilisées, pour éviter l'effet d'antenne. Voir cette référence 604267116629

- L'ESP32 est le modèle le plus standard possible celui qui a 38 broches attention, les ESP32 avec prise USB C semble ne pas avoir le même espacement entre broches. prenez la version avec un port micro usb.

- Le reste des composants est tout ce qu'il y a plus de standard
Renforcement des pistes de puissance
Il est impératif de doubler à l'aide d'un fil de cuivre de section 1.5mm² la partie puissance.
Voir photo ci-dessous, les deux pistes en question ne sont pas recouvertes du vernis de protection afin de rendre la soudure plus facile.

Soudage des composants
Je vous recommande d'utiliser des barrettes de connexion pour l'ESP32. car la programmation initiale (identification de l'adresse du DS18b20) sera à faire sans que l'esp soit installé sur le circuit.
Commencez par les composants les plus petits, (résistances) puis allez progressivement vers les plus gros. Concernant les résistances des 1/4 de watts suffisent, SAUF pour les deux résistances (R48 et R410) de 120k qui doivent OBLIGATOIREMENT être des résistances d'1 watt minimum.
Les LEDs
3 LED sont présentes sur le circuit. Voici leurs significations :

- LED 3/OVER TEMP : est connectée au GPIO 25
red_led_pin
elle indique un problème ( surchauffe ou température invalide ) - LED2 / PROD : est connectée au GPIO32
green_led_pin
elle indique que le dimmer est actif - LED TIC : cette dernière clignote à la réception des trames télé-information.
Je vous conseille d'utiliser des fils dupond de 20cm mini et procéder comme pour la sonde de température (voir ci-dessous)
Radiateur et triac
Le triac (module de puissance) sera refroidi par un radiateur de 40 x 40 x 100 mm

Je vous conseille de procéder dans l'ordre suivant :
- identifier le centre du radiateur, puis faire un trou et un taraudage en 4 mm,
- fixer le triac temporairement à l'aide d'une vis de M4,
- présenter le tout sur le circuit imprimé ( ne pas encore souder le triac) afin de pointer les deux trous permettant la fixation du radiateur sur le circuit imprimé. Percez et taraudez toujours en 4m.

D'autre part pour assurer une meilleure isolation électrique entre le radiateur et les pistes du triac, à l'aide d'une lime faite un chanfrein tel que ci-dessous :

Vous pouvez même mettre du vernis à ongle sur les pattes du triac.

Sonde de température (DS18B20)
Pour la surveillance de la température du triac, en effet il y a de la puissance donc de la chaleur à évacuer : un circuit de protection est indispensable.
Pour information voici la courbe de température du triac quand je fais un complément de chauffage la nuit (+ 40 °C en 1 heure)

Ce circuit de protection est basé sur une sonde de température dallas DS18B20, qui sera installée dans le radiateur au plus proche du triac. En effet passé une certaine température, la température de fonctionnement max du triac, celui-ci va rendre l'âme. Dans notre cas, il est préférable de maintenir une température de jonction à une température raisonnable, ce qui fait que je limite la température du radiateur à 75°, d'autant plus que le radiateur n'est pas protégé. Donc s'il est trop chaud potentiellement brûlure.
Pour fixer la sonde, un trou du diamètre de la sonde Dallas est fait au plus proche de la fixation du triac.
Le problème des boitiers TO92 ce n'est pas un diamètre standard (4.7mm). J'ai percé à 4.5 mm puis avec une petite lime ou une petite fraise, puis j'ai adapté le trou de telle sorte que la sonde puise être installée un peu en force.
La sonde fixée avec de la Superglue ou de la colle bi-composant époxy. Avant fixation définitive, je m'assure que cette sonde fonctionne correctement (identification de l'adresse) et je soude des fils Dupont auxquels j'aurais préalablement enlevé les protections en plastique.
Avant soudure, j'aurais fait la provision de gaine thermo rétractable de longueur suffisante. la sonde sera branchée sur le connecteur à trois broches qui est au centre du circuit imprimé ayant comme repère VCC Dout GND. (j'ai fait la même chose pour les leds)

Avant mise sous tension, à l'aide d'un ohmmètre, assurez-vous de la parfaite isolation du radiateur et des broches du triac.
Mesure du courant
Ce module contient un élément de mesure JSY-MK-194T. Il est disponible chez Aliexpress. Grâce à ce dispositif les grandeurs suivantes sont récupérées, tension, courant, puissance, "sens du courant" pour deux circuits. Voici un lien pour la documentation

La première (celle du bas) pour la mesure de la puissance récupérée, donc l'énergie injectée dans le chauffe-eau. Elle ne sert qu'à cela.
La deuxième sonde (celle du haut) sera utilisée pour la mesure du circuit EDF et participe intégralement à la régulation. Elle est à mettre sur la phase du disjoncteur principal de votre installation électrique. Attention elle a un sens, voir explications plus bas
Il en existe plusieurs modèles du module JSY-MK-194T :


Prenez de préférence la première version.
Partie logicielle
Pour le soft, comme indiqué en préambule, je me suis largement inspiré du travail de rem81, que j'ai adapté à mes besoins.
Les chapitres qui suivent présentent le code ESPHome de chaque module. Je vais essayer d'expliquer étape par étape comment cela fonctionne.
La partie téléformation
Là je n'ai rien inventé tout un tas d'articles sont présents sur le forum HACF.
Il y a juste une subtilité : je voulais afficher les valeurs de consommation journalière HP et HC. Or il n'y a pas de fonction UTILITY_METER dans ESPHOME, j'ai donc bricolé un truc, qui simule tant bien que mal cette fonction. J'aurais pu faire le calcul dans HA et le rebasculer dans l'ESP.
esphome:
name: "router"
friendly_name: router-tic-4ct
on_boot:
priority: 800
then:
# on initialise a zero old_hp et old_hc a chaque demarrage car il n'y a pas de sauvegarde de la valeur
- sensor.template.publish:
id: old_hc
state : 0
- sensor.template.publish:
id: old_hp
state : 0
substitutions:
rxd_teleinfo_pin: GPIO3
uart:
teleinfo:
id: myteleinfo
update_interval: 5s
historical_mode: true
uart_id: uart_teleinformation
binary_sensor:
# identification du mode heure pleine heure creuse utilisé pour forcer la chauffe la nuit et en heure creuse
- platform: template
name: "Sensor Heure Pleine"
id: mode_hp
sensor:
# Simulation d'un utility meter pour la consommation heure creuse et heure pleine avec un reset a minuit; cette fonction n'existe pas dans esphome
- platform: template
name: "HEURE CREUSE JOUR"
id: hcj
unit_of_measurement: "kwh"
lambda: return (float(id(thc).state) - float(id(old_hc).state))/1000;
device_class: energy
update_interval: 60s
- platform: template
name: "HEURE PLEINE JOUR"
id: hpj
unit_of_measurement: "kwh"
lambda: return (float(id(thp).state) - float(id(old_hp).state))/1000;
device_class: energy
update_interval: 60s
- platform: template
name : "old_hc"
id: old_hc
- platform: template
name : "old_hp"
id: old_hp
# Declarations pour la teleinformation
#Linky BASE value
- platform: teleinfo
tag_name: "HCHC"
id: thc
name: "Total Heure Creuse"
filters:
- filter_out: 0
unit_of_measurement: "Wh"
icon: mdi:counter
teleinfo_id: myteleinfo
device_class: energy
on_value:
then:
- if:
condition:
lambda: 'return id(old_hc).state == 0;'
then:
- sensor.template.publish:
id: old_hc
state: !lambda |-
return id(thc).state;
- platform: teleinfo
tag_name: "HCHP"
id: thp
name: "Total Heure Pleine"
filters:
- filter_out: 0
unit_of_measurement: "Wh"
icon: mdi:counter
teleinfo_id: myteleinfo
device_class: energy
on_value:
then:
- if:
condition:
lambda: 'return id(old_hp).state == 0;'
then:
- sensor.template.publish:
id: old_hp
state: !lambda |-
return id(thp).state;
#Linky Consumption
- platform: teleinfo
tag_name: "PAPP"
name: "Linky Consommation"
unit_of_measurement: "VA"
icon: mdi:flash
teleinfo_id: myteleinfo
id: papp
#Linky Intensity
- platform: teleinfo
tag_name: "IINST"
name: "Linky Intensité"
unit_of_measurement: "A"
icon: mdi:flash
teleinfo_id: myteleinfo
#sensor PTEC puisssance tarrifaire en cours
text_sensor:
- platform: teleinfo
tag_name: "PTEC"
name: "ptec"
id: ptec
teleinfo_id: myteleinfo
script:
# on initialise old_hc et old_hp le soir a minuit 59 minutes afin de simuler un semblant de utility sensor,
- id: reset_hphc
mode : single
then :
- sensor.template.publish:
id: old_hc
state: !lambda |-
return id(thc).state;
- sensor.template.publish:
id: old_hp
state: !lambda |-
return id(thp).state;
# test heure creuse
- id: test_heure_creuse
mode: single
then:
- if:
condition:
lambda: 'return id(ptec).state == "HC..";'
then:
- binary_sensor.template.publish:
id: mode_hp
state: OFF
- logger.log:
format: "Passage Heure Creuse"
level: "info"
- if:
condition:
lambda: 'return id(ptec).state == "HP..";'
then:
- binary_sensor.template.publish:
id: mode_hp
state: ON
- logger.log:
format: "Passage Heure Pleine"
level: "info"
La partie sondes de courant
La partie sondes de courant nécessite entre autre l'initialisation du bus I²C ainsi que la déclaration des 2 modules ADS 11115.
On récupérera donc, pour chacun des circuits considéré :
- le courant
- la puissance
- et l'énergie (celle-ci est remise à zéro à minuit)
substitutions:
# bus i²c
sda_pin: GPIO21
scl_pin: GPIO22
# activation de l'interface i²c
i2c:
sda: ${sda_pin}
scl: ${scl_pin}
scan: True
id: i2c_bus_1
frequency: 200khz
# on active les 2 modules ADC necessaire pour les 4 sondes de courant
ads1115:
- address: 0x48
id : ads1
continuous_mode: true
- address: 0x49
id : ads2
continuous_mode: true
sensor:
#definition des sensors pour les 4 sondes de courant additionnelles
# channel A
- platform: ads1115
ads1115_id: ads2
multiplexer: 'A0_A1'
gain: 1.024
name: "Channel A"
force_update: true
id: Channel_A
internal: true
- platform: ct_clamp
sensor: Channel_A
name: "Measured Current Channel A"
update_interval: ${update_freq}
accuracy_decimals: 2
filters:
- calibrate_linear:
- 0 -> 0
- 1 -> 30
id: current_channel_A
- platform: template
name: power channel a
id: power_channel_a
update_interval: ${update_freq}
unit_of_measurement: W
lambda: return id(current_channel_A).state * id(tension).state ;
- platform: total_daily_energy
name: energy channel a
power_id: power_channel_a
id: energy_channel_a
unit_of_measurement: kWh
device_class: energy
filters:
- multiply: .001
# channel B
- platform: ads1115
ads1115_id: ads2
multiplexer: 'A2_A3'
gain: 1.024
name: "Channel A"
force_update: true
id: Channel_B
internal: true
- platform: ct_clamp
sensor: Channel_B
name: "Measured Current Channel B"
update_interval: ${update_freq}
accuracy_decimals: 2
filters:
- calibrate_linear:
- 0 -> 0
- 1 -> 30
id: current_channel_B
- platform: template
name: power channel b
id: power_channel_b
update_interval: ${update_freq}
unit_of_measurement: W
lambda: return id(current_channel_B).state * id(tension).state;
- platform: total_daily_energy
name: energy channel b
power_id: power_channel_b
id: energy_channel_b
unit_of_measurement: kWh
device_class: energy
filters:
- multiply: .001
# channel C
- platform: ads1115
ads1115_id: ads1
multiplexer: 'A0_A1'
gain: 1.024
name: "Channel C"
force_update: true
id: Channel_C
internal: true
- platform: ct_clamp
sensor: Channel_C
name: "Measured Current Channel C"
update_interval: ${update_freq}
accuracy_decimals: 2
filters:
- calibrate_linear:
- 0 -> 0
- 1 -> 30
id: current_channel_C
- platform: template
name: power channel c
id: power_channel_c
update_interval: ${update_freq}
unit_of_measurement: W
lambda: return id(current_channel_C).state * id(tension).state;
- platform: total_daily_energy
name: energy channel c
power_id: power_channel_c
id: energy_channel_c
unit_of_measurement: kWh
device_class: energy
filters:
- multiply: .001
# channel D
- platform: ads1115
ads1115_id: ads1
multiplexer: 'A2_A3'
gain: 1.024
name: "Channel D"
force_update: true
id: Channel_D
internal: true
- platform: ct_clamp
sensor: Channel_D
name: "Measured Current Channel D"
update_interval: ${update_freq}
accuracy_decimals: 2
filters:
- calibrate_linear:
- 0 -> 0
- 1 -> 30
id: current_channel_D
- platform: template
name: power channel d
id: power_channel_d
update_interval: ${update_freq}
unit_of_measurement: W
lambda: return id(current_channel_D).state * id(tension).state;
- platform: total_daily_energy
name: energy channel d
power_id: power_channel_d
id: energy_channel_d
unit_of_measurement: kWh
device_class: energy
filters:
- multiply: .001
NOTA : Si vous souhaitez changer la valeur d'une sonde de courant, modifiez la partie du code correspondante
# si channel A avec une sonde 15 Amperes modifier la ligne 20 en y mettant la valeur maximum de la sonde c'est a dire 15
# a adapter pour chacune des voies pour laquelle vous avez changé de sonde de courant, en fonction de la sonde utilisée
- platform: ads1115
ads1115_id: ads2
multiplexer: 'A0_A1'
gain: 1.024
name: "Channel A"
force_update: true
id: Channel_A
internal: true
- platform: ct_clamp
sensor: Channel_A
name: "Measured Current Channel A"
update_interval: ${update_freq}
accuracy_decimals: 2
filters:
- calibrate_linear:
- 0 -> 0
- 1 -> 15
# modification à faire ci dessus en fonction de la sensibilité de la sonde
id: current_channel_A
- platform: template
name: power channel a
id: power_channel_a
update_interval: ${update_freq}
unit_of_measurement: W
lambda: return id(current_channel_A).state * id(tension).state ;
- platform: total_daily_energy
name: energy channel a
power_id: power_channel_a
id: energy_channel_a
unit_of_measurement: kWh
device_class: energy
filters:
- multiply: .001
Le variateur de puissance (dimmer)
Le code utilisé par le dimmer est celui de la doc d'ESPHome, je n'ai rien inventé.
Un gradateur à triac est simplement un circuit qui est chargé de découper le signal du secteur afin d’en réduire sa puissance moyenne. Pour faire simple, on va découper des morceaux de l’alimentation d’une charge purement résistive (le chauffe-eau) afin de pouvoir moduler la puissance injectée.
La valeur 'a' est le temps de déclenchement, plus cette valeur est petite plus longtemps le triac sera passant et du coup plus longtemps le chauffe-eau sera alimenté.
Dans notre cas la valeur de a = (100 - sortie_triac).

Voici le code du variateur :
substitutions:
gate_pin: GPIO33
zerocross_pin: GPIO34
output:
- platform: ac_dimmer
id: dimmer_ecs
gate_pin: ${gate_pin}
zero_cross_pin:
number: ${zerocross_pin}
mode:
input: true
inverted: yes
min_power: 0.1
light:
- platform: monochromatic
name: "Dimmer"
output: dimmer_ecs
name: Dimmerized Light
id: gradateur
default_transition_length: 50ms
Bien comprendre l'identification de la valeur PMAX sinon le routeur se mettra en défaut jusqu'à ce que la température du radiateur revienne à un niveau acceptable ( tmax -2 °) idéalement Tmax = 70°C
RÉGULATION
La solution retenue pour la régulation est une régulation proportionnelle. Le système n'est pas linéaire car on "découpe" une sinusoïde.
La régulation est faite en mesurant la puissance traversant le Linky :
- Si elle est positive, alors on consomme (la production solaire est insuffisante).
- Si elle est négative, alors on injecte de l'énergie dans le circuit EDF.
Si les mesures indiquent le contraire, inverser le sens de la pince qui est sur la phase en sortie du disjoncteur principal.
La valeur d'ouverture du triac (de 0 à pmax) est calculée de la manière suivante :
incrément= (puissance_reseau) *coef/1000*(-1)
Et ensuite :
- Si la puissance réseau est négative (on surproduit) alors l'incrément est positif
- Si la puissance réseau est positive (on consomme) alors l'incrément est négatif
Une fois le calcul précédent fait, on affecte à la valeur d'ouverture du triac la valeur précédente d'ouverture + l'incrément calculé.
# calcul de la valeur d'injection
- id: calcul_injection
mode: single
then:
# mode jour routage si mode auto activé
- if:
condition:
and:
- binary_sensor.is_on : temperature_triac_ok
- binary_sensor.is_off : mode_nuit
- switch.is_on : modeauto
then:
- lambda: |-
id(increment) = (id(puissance_reseau).state*id(coeff_r).state)/1000*-1;
- lambda: |-
id(sortie_triac) = id(sortie_triac)+id(increment);
if (!isnan(id(sortie_triac))) {
id(sortie_triac) = id(sortie_triac)+id(increment);
}else{
id(sortie_triac)=0;
}
if (id(sortie_triac) <= 0){
id(sortie_triac) = 0;
} else if(id(sortie_triac)>=id(pmax).state){
id(sortie_triac) = id(pmax).state;
}
- light.turn_on:
id: gradateur
brightness: !lambda |-
return id(sortie_triac)/100 ;
- output.turn_on: led_green
- logger.log:
format: "Log Auto OK sortie Triac %f - Increment %f"
args: [ 'id(sortie_triac)', 'id(increment)' ]
level: info
- lambda: |-
id(affichage_sortie_triac).publish_state( id(sortie_triac) );
id(affichage_increment).publish_state( id(increment) );
# mode nuit heure pleine on desactive le gradateur
- if:
condition:
and:
- binary_sensor.is_on: mode_nuit
- binary_sensor.is_on : mode_hp
then:
- lambda: |-
id(sortie_triac) = 0;
id(increment) = 0;
- light.turn_off:
id: gradateur
- output.turn_off: led_green
- logger.log:
format: "Log mode nuit heure pleine"
level: info
# mode nuit heure creuse on active le gradateur si mode auto
- if:
condition:
and:
- binary_sensor.is_on: mode_nuit
- binary_sensor.is_off : mode_hp
- switch.is_on : modeauto
then:
- lambda: |-
id(sortie_triac) = id(pmax).state;
id(increment) = 0;
- light.turn_on:
id: gradateur
brightness: !lambda |-
return id(sortie_triac)/100 ;
- output.turn_on: led_green
- logger.log:
format: "Log mode nuit heure creuse sortie triac %f"
args: [ 'id(sortie_triac)']
level: info
# il y a un probleme on desactive le gradateur
- if:
condition:
or:
- binary_sensor.is_off : temperature_triac_ok
- switch.is_off: modeauto
then:
- lambda: |-
id(sortie_triac) = 0;
id(increment) = 0;
- light.turn_off:
id: gradateur
- output.turn_off: led_green
- logger.log:
format: "Log mode manuel off ou surchauffe"
level: info
- lambda: |-
id(affichage_sortie_triac).publish_state( id(sortie_triac) );
id(affichage_increment).publish_state( id(increment) );
De fait, j'ai très peu de moments où je dois compléter par le réseau EDF (overshoot) lorsque j'injecte dans le chauffe-eau. C'est appréciable car je suis en tout électrique à la maison.
Mesure des courants pour le routeur
Ce module communique via le protocole MODBUS via une liaison série. Il est donc nécessaire de procéder à l'initialisation de ce protocole via le code suivant.
Une des sondes sera utilisée pour mesurer le courant circulant dans la phase issue du compteur général ( sortie du Linky et avant la répartition sur les rails de disjoncteurs).
substitutions:
rxd_pin: GPIO16
txd_pin: GPIO17
uart:
id: mod_bus
tx_pin: ${txd_pin}
rx_pin: ${rxd_pin}
baud_rate: 38400
stop_bits: 1
modbus:
id: modbus1
modbus_controller:
- id: jsymk
address: 0x1
modbus_id: modbus1
update_interval: 0.75s
command_throttle: 50ms
sensor:
# Puissance traversant la sonde 1 / ecs
- platform: modbus_controller
modbus_controller_id: jsymk
id: puissance_ecs
name: "Puissance ECS"
address: 0x004A
unit_of_measurement: "W"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
# Puissance traversant la sonde 2 / Reseau EDF
- platform: modbus_controller
modbus_controller_id: jsymk
id: puissance_reseau_absolue
name: "Puissance Reseau Absolue"
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(sens_pince).state == 1 ) {
id(puissance_reseau).publish_state( id(puissance_reseau_absolue).state *-1);
} else {
id(puissance_reseau).publish_state( id(puissance_reseau_absolue).state );
}
# determination si injection ou consommation : sens pince = 1 alors excedent de production sens pince = 0 consommation sur le reseau
- platform: modbus_controller
modbus_controller_id: jsymk
id: sens_pince
name: "Sens_Pince"
address: 0x004E
register_type: holding
value_type: U_DWORD
bitmask: 0X00010000
filters:
- multiply: 1
register_count: 1
response_size: 4
# Affichage Puissance Reseau
- platform: template
name: "Puissance Reseau"
id: puissance_reseau
unit_of_measurement: "W"
state_class: "measurement"
# calcul de l'energie injectée dans le cumulus pour le jour en cours remis a zero a minuit
- platform: total_daily_energy
name: "Energie Injectée ECS"
id: energy_ecs
power_id: puissance_ecs
unit_of_measurement: kWh
device_class: energy
filters:
- multiply: .001
Mesure de température du triac (DS18B20)
Comme nous l'avons vu, une sonde de température mesure la température du triac pour en assurer la protection.
Le capteur DS18B20 est un capteur de température qui utilise un bus ONE-WIRE/ 1-wire fabriqué originalement par Dallas. Cette technologie de capteur permet d'empiler plusieurs capteurs à travers un bus. Donc il est nécessaire de pouvoir identifier un capteur dans cette chaine parmi les autres : cela est fait en utilisant son adresse interne.
Dans ESPHOME , il est nécessaire de déclarer l'utilisation du module 1-wire et ensuite de déclarer un sensor Dallas avec son adresse spécifique, même si nous n'utilisons qu'un seul et unique capteur.
La première étape est donc l'identification de l'adresse de la sonde
j'avais un ESP8266 sous le coude le code est quasi similaire pour un esp32, il faut juste changer le type de microcontrôleur.
esphome:
name: temp_ds18b20
esp8266:
board: nodemcu
# Enable logging
logger:
# Enable Home Assistant API
api:
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
captive_portal:
one_wire:
- platform: gpio
pin: D6
En retour l'ESP donnera dans les logs quelque chose comme cela :
INFO ESPHome 2024.11.2
INFO Reading configuration /config/esphome/temp-boiler.yaml...
INFO Starting log output from 192.168.1.42 using esphome API
INFO Successfully connected to temp-boiler @ 192.168.1.42 in 0.008s
INFO Successful handshake with temp-boiler @ 192.168.1.42 in 0.018s
[21:30:01][I][app:100]: ESPHome version 2024.11.2 compiled on Nov 28 2024, 21:28:22
[21:30:01][C][wifi:600]: WiFi:
[21:30:01][C][wifi:428]: Local MAC: AA:AA:AA:AA:AA:AA
[21:30:01][C][wifi:433]: SSID: [redacted]
[21:30:01][C][wifi:436]: IP Address: 192.168.1.xxx
[21:30:01][C][wifi:439]: BSSID: [redacted]
[21:30:01][C][wifi:441]: Hostname: 'temp_ds18b20'
[21:30:01][C][wifi:443]: Signal strength: -59 dB ▂▄▆█
[21:30:01][C][wifi:447]: Channel: 2
[21:30:01][C][wifi:448]: Subnet: 255.255.255.0
[21:30:01][C][wifi:449]: Gateway: 192.168.1.254
[21:30:01][C][wifi:450]: DNS1: 192.168.1.254
[21:30:01][C][wifi:451]: DNS2: 0.0.0.0
[21:30:01][C][logger:185]: Logger:
[21:30:01][C][logger:186]: Level: DEBUG
[21:30:01][C][logger:188]: Log Baud Rate: 115200
[21:30:01][C][logger:189]: Hardware UART: UART0
[21:30:01][C][gpio.one_wire:020]: GPIO 1-wire bus:
[21:30:01][C][gpio.one_wire:021]: Pin: GPIO06
[21:30:01][C][gpio.one_wire:080]: Found devices:
[21:30:01][C][gpio.one_wire:082]: 0x9b3ce1e3814f3128 (DS18B20)
Dans ce cas l'adresse du capteur de température est 0x9b3ce1e3814f3128
Par acquis de conscience, je fais un petit essai pour vérifier le bon fonctionnement de la sonde.
substitutions:
dallas_address: "0x9b3ce1e3814f3128"
dallas_pin: GPIO6
esphome:
name: temp_ds18b20
esp8266:
board: nodemcu
logger:
api:
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
captive_portal:
one_wire:
- platform: gpio
pin: ${dallas_pin}
sensor:
- platform: dallas_temp
address: ${dallas_address}
name: "temperature triac"
update_interval: 2s
id: temperature_triac
Petite subtilité afin d'endurcir le système si le capteur de température n'est pas initialisé, faux contact, mauvaise adresse, celui-ci retourne NAN (Not A Number) cette dernière valeur sera utilisée dans un script pour dévalider le routage.
# Si Temp Triac invalide
- if:
condition:
lambda: 'return (isnan(id(temperature_triac).state));'
then:
- binary_sensor.template.publish:
id: temperature_triac_ok
state: OFF
- light.turn_off:
id: gradateur
- logger.log:
format: "Log Temperature triac invalide"
level: "info"
- output.turn_on: led_red
L'adresse identifiée est à utiliser dans le code final ci-dessous, et à mettre à la place du code donné pour exemple.
Gestion des heures de fonctionnement (Sun)
Pour utiliser ce module, il est nécessaire d'avoir une horloge afin de procéder aux calculs de l'azimut et de l'élévation, mais surtout du mode jour ou nuit.
Dans mon cas, je récupère l'heure de Home Assistant et je définis ma position par des substitutions. Vous trouverez facilement les deux valeurs à l'aide de Google maps. Elles seront à adapter dans le début du code.
substitutions:
time_timezone: "Europe/Paris"
ma_latitude: "43.14°"
ma_longitude: "3.14°"
time:
- platform: homeassistant
id: my_time
sun:
latitude: ${ma_latitude}
longitude: ${ma_longitude}
Grâce à un script, je définis si c'est le jour ou la nuit, et tout cela est affecté dans un binary sensor grâce au code suivant. J'en aurai besoin plus tard si je n'ai pas réussi à charger assez mon chauffe-eau.
binary_sensor:
- platform: template
name: "Mode-nuit"
id: mode_nuit
script:
- id: test_nuit
mode: single
then:
- if:
condition:
sun.is_below_horizon
then:
- binary_sensor.template.publish:
id: mode_nuit
state: ON
- logger.log:
format: "Passage Mode Nuit"
level: "info"
- if:
condition:
sun.is_above_horizon
then:
- binary_sensor.template.publish:
id: mode_nuit
state: OFF
- logger.log:
format: "Passage Mode Jour"
level: "info"
Je récupère ensuite l'heure de mon serveur home assistant. C'est indispensable pour le module SUN pour qu'il puisse identifier si le soleil est au-dessous ou au-dessus de l'horizon.
time:
- platform: homeassistant
id: my_time
Tarification HC / HP
J'ai un abonnement avec heure pleine heure/creuse. Et comme nous avons une interface linky intégrée, nous pouvons donc récupérer la tarification en cours (paramètre PTEC) qui a pour valeur HC.. ou HP..
Pour cela, je rajoute le code ESP suivant, Bien entendu cela est à adapter en fonction du mode de votre linky et de votre contrat de fourniture d’électricité.
text_sensor:
# Puissance Tarifaire en cours
- platform: teleinfo
tag_name: "PTEC"
name: "ptec"
id: ptec
teleinfo_id: myteleinfo
script:
- id: test_heure_creuse
mode: single
then:
- if:
condition:
lambda: 'return id(ptec).state == "HC..";'
then:
- binary_sensor.template.publish:
id: mode_hp
state: OFF
- logger.log:
format: "Passage Heure Creuse"
level: "info"
- if:
condition:
lambda: 'return id(ptec).state == "HP..";'
then:
- binary_sensor.template.publish:
id: mode_hp
state: ON
- logger.log:
format: "Passage Heure Pleine"
level: "info"
Routage automatique ou non
J'ai décidé de pouvoir activer ou non la fonction routage automatique la journée ou forcée la nuit pour des raisons X ou Y. Un binary_sensor sera interne à l'ESP mais commandé par une automation HA.
Si j'injecte dans le réseau ENEDIS une certaine valeur, c'est que mon Chauffe Eau est "plein".
Dernière chose : j'ai installé forecast.solar qui me permet d'avoir une idée de la prévision de production d'énergie solaire via l'entité : sensor.energy_production_tomorrow
Donc j'ai fait dans Home Assistant 2 automations, (les puristes pourront en faire une seule et unique, mais là un choix).
La première désactive le complément de chauffe la nuit si on a réussi à injecter si j'ai injecté plus de 2kWh dans le réseau Enedis.
alias: routeur - mode auto off
description: ""
triggers:
- trigger: time
at: "23:59:00"
conditions:
- condition: numeric_state
entity_id: sensor.injection_jour
above: 2000
- condition: state
entity_id: input_boolean.inter_validation_routeur
state: "off"
actions:
- action: switch.turn_off
metadata: {}
data: {}
target:
entity_id:
- switch.router_tic_4ct_mode_auto
- action: notify.pushbullet
metadata: {}
data:
message: mode auto routeur off
mode: single
Pour l'identification de la valeur d'injection je vous laisse regarder si votre systeme photovoltaique vous le propose.
Sinon à l'aide d'un template... ( merci https://forum.hacf.fr/t/panneaux-solaire-enphase-envoy/3938/8)
template:
- sensor:
- name: injection
unit_of_measurement: "W"
state: "{{ states('sensor.solar_power_production') | float | round(2) - states('sensor.current_power_consumption') | float | round(2)}}"
et le matin, on réactive le routeur, si le boolean inter_validation_router est activé dans Home Assistant. Ce dernier est un helper créé pour activer ou non le routage. C'est un peu comme un mode vacances. pourquoi chauffer l'eau alors que nous sommes absents.
alias: routeur mode auto on
description: ""
triggers:
- trigger: state
entity_id:
- sun.sun
from: below_horizon
to: above_horizon
conditions:
- condition: state
entity_id: input_boolean.inter_validation_routeur
state: "on"
actions:
- action: switch.turn_on
metadata: {}
data: {}
target:
entity_id: switch.mode_auto
mode: single
En effet, si la météo a été maussade, j'ai besoin d'un complètement d'injection avec une puissance ayant une valeur prédéterminée (PMAX).
Identification de Pmax
La valeur de PMAX est à identifier manuellement de telle sorte que le circuit de protection thermique ne soit pas activé.
Je m'explique, en ce moment la température dans mon garage est d'environ 12°c, et quand j'active la marche forcée, (soit 80% d'ouverture et grosso modo 2 kWatt mesuré) la température de mon triac atteint 50-55°c, soit une élévation de température de grosso modo 40°c pour une injection à 80%.
En été la température dans mon garage est aux alentours de 30-35° la journée, donc la température du triac peut facilement atteindre 75°c/80°c. Du coup, j'arrive facilement à la limite de sécurité. A moi en été de limiter la valeur de Pmax.
Ci-dessous la partie de code ESP pour le complément de chauffe la nuit si nécessaire.
- if:
condition:
and:
- binary_sensor.is_on: mode_nuit
- binary_sensor.is_off : mode_hp
- switch.is_on : modeauto
then:
- lambda: |-
id(sortie_triac) = id(pmax).state;
id(increment) = 0;
- light.turn_on:
id: gradateur
brightness: !lambda |-
return id(sortie_triac)/100 ;
- output.turn_on: led_green
- logger.log:
format: "Log mode nuit heure creuse sortie triac %f"
args: [ 'id(sortie_triac)']
level: info
Affichage sur un écran LCD 20* 4

L'écran LCD est complètement inutile (tout est dans Home Assistant) donc rigoureusement indispensable. Il est connecté via le BUS I²C, qui exposé en bas du circuit imprimé. 4 fils Dupond permettrons de raccorder les 2

GND va sur GND ; SDA va sur SDA ; SCL va sur SCL ; VCC va sur VCC
Affichage des données sur 5 pages d'informations avec changement toutes les 5 secondes
globals:
- id: page
type: int
initial_value: "1"
display:
- platform: lcd_pcf8574
id: mydisplay
dimensions: 20x4
address: 0x27
lambda: |-
switch (id(page)){
case 1 :
it.strftime(2, 0, "%H:%M %d-%m-%Y", id(ha_time).now());
it.printf(0,1,"tarif:%s", id(ptec).state.c_str() );
it.printf(16,1,"%s", id(mode_nuit).state ? "Nuit" : "Jour");
it.printf(0,2,"uptime %0.f H",id(esp_uptime).state);
it.printf(0,3,"Wifi:%0.1f dB", id(wifi_signal_db).state);
break;
case 2 :
it.strftime(2, 0, "%H:%M %d-%m-%Y", id(ha_time).now());
it.printf(0,1,"P Reseau=%0.0f W",id(puissance_reseau).state);
it.printf(0,2,"P ECS=%0.0f W ",id(puissance_ecs).state);
it.printf(0,3,"Energie ECS=%0.3f kWh",id(energy_ecs).state);
break;
case 3 :
it.strftime(2, 0, "%H:%M %d-%m-%Y", id(ha_time).now());
it.printf(0,1,"Triac T=%0.1f C", id(temperature_triac).state);
it.printf(10,2,"Ouv T:%0.0f", id(affichage_sortie_triac).state);
it.printf(0,2,"Inc %0.1f", id(affichage_increment).state);
it.printf(0,3,"Mode=%s", id(modeauto).state ? "Auto" : "Manu");
break;
case 4 :
it.strftime(2, 0, "%H:%M %d-%m-%Y", id(ha_time).now());
it.printf(0,1,"hpj=%0.3f kWh", id(hpj).state);
it.printf(0,2,"hcj=%0.3f kWh", id(hcj).state);
it.printf(0,3,"papp:%0.0f VA", id(papp).state);
it.printf(10,3,"tarif:%s", id(ptec).state.c_str() );
break;
case 5 :
it.printf(0, 0, "PA:%0.0f W",id(power_channel_a).state);
it.printf(0, 1, "PB:%0.0f W",id(power_channel_b).state);
it.printf(0, 2, "PC:%0.0f W",id(power_channel_c).state);
it.printf(0, 3, "PD:%0.0f W",id(power_channel_d).state);
it.printf(9, 0, "EA:%0.3fkWh",id(energy_channel_a).state);
it.printf(9, 1, "EB:%0.3fkWh",id(energy_channel_b).state );
it.printf(9, 2, "EC:%0.3fkWh",id(energy_channel_c).state );
it.printf(9, 3, "ED:%0.3fkWh",id(energy_channel_d).state);
break;
} interval:
- interval: 5s
then:
- lambda: |-
id(page) = (id(page) + 1);
if (id(page) > 3) {
id(page) = 1;
Le script associé permet de basculer automatiquement parmi les 5 pages définies ci-dessus avec un intervalle de 5 secondes.
TIPS - sauvegarder le code de l'ESP
le code est stocké en clair dans l'ESP et lisible grâce à l'utilisation d'un external_components , Merci @tedour pour avoir trouvé cela.
Pour récupérer le fichier source http://ip_esp/config.yaml
# definition external componant
# permet de faire un backup du code dans l'ESP
external_components:
- source: github://dentra/esphome-components
backup:
Le code complet
Voici le code qui est actif actuellement sur mon routeur :
# definition des substitutions
# ATTENTION IL EST INDISPENSABLE D'IDENTIFIER L'ADRESSE DE SON CAPTEUR DALLAS voir ESPHOME.IO pour la marche a suivre
# ATTENTION LA LONGITUDE ET LA LATITUDE DOIVENT ETRE ELLES AUSSI CUSTOMISEE.
# Cela permet de definir un boolean pour le complement de chauffe la nuit.
# Attention j'utilise aussi un sensor de HA issu de la teleinfo afin de d'activer le routeur la nuit lors du passage en heure creuse
substitutions:
friendly_name: myrouter2
gate_pin: GPIO33
zerocross_pin: GPIO34
green_led_pin: GPIO32
red_led_pin: GPIO25
wifi_led_pin: GPIO13
dallas_pin: GPIO27
dallas_address: "0xbf0008008afd4a10"
sda_pin: GPIO21
scl_pin: GPIO22
rxd_pin: GPIO16
txd_pin: GPIO17
time_timezone: "Europe/Paris"
ma_latitude: "43.659°"
ma_longitude: "1.310°"
adress_ip: "192.168.1.219"
# definition du type de esp et du framework
esp32:
board: nodemcu-32s
framework:
type: arduino
# desactivation des sorties au boot de l'ESP
esphome:
name: ${friendly_name}
on_boot:
priority: 800
then:
- binary_sensor.template.publish:
id: temperature_triac_ok
state: OFF
- light.turn_off : gradateur
- output.turn_off: led_green
- output.turn_off: led_red
# activation de la fonction log pour deboggage
logger:
baud_rate: 0
# level: debug
level: INFO
# activation de l'api de communication entre ESPHOME et HOME ASSISTANT
api:
# activation du wifi et de l'access point mode si probleme de connection
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
reboot_timeout: 5min
ap:
ssid: "Myrouter Fallback Hotspot"
password: !secret wifi_password
captive_portal:
# activation de la fonction Over The Air Update
ota:
- platform: esphome
# activation du server web interne
web_server:
port: 80
# activation de l'horloge interne
# necessaire pour identifier l'heure du couché du soleil, utilisé par le composant SUN
time:
- platform: homeassistant
id: my_time
# activation du componant sun pour identifier si jour ou nuit
sun:
latitude: ${ma_latitude}
longitude: ${ma_longitude}
# definition external componant
# permet de faire un backup du code dans l'ESP
external_components:
- source: github://dentra/esphome-components
backup:
#activation du componant I²C necessaire pour l'ecran LCD
i2c:
sda: ${sda_pin}
scl: ${scl_pin}
scan: True
id: i2c_bus_1
#activation du componant UART indispensable pour le componant MODBUS
uart:
id: mod_bus
tx_pin: ${txd_pin}
rx_pin: ${rxd_pin}
baud_rate: 38400
stop_bits: 1
# Activation du composant modbus necessaire pour le capteur jsymk
modbus:
id: modbus1
# definition du controleur modubs
modbus_controller:
- id: jsymk
address: 0x1
modbus_id: modbus1
update_interval: 0.75s
command_throttle: 50ms
# activation du composant on_wire indispensable pour la sonde de temperature dallas
one_wire:
- platform: gpio
pin: ${dallas_pin}
#Definition des variables globales
globals:
# increment est la variable qui est utilisée pour le calcul de la variation de la valeur d'ouverture du triac
- id: increment
type: float
restore_value: no
initial_value: '0'
# Sortie triac est la valeur donnee au dimmer
- id: sortie_triac
type: float
initial_value: '0'
- id: page
type: int
initial_value: "1"
# Definition des binary sensors
binary_sensor:
# donne l'etat de connection wifi
- platform: status
name: "Status"
# indique si la temperature du triac est inferieure a la valeur maximum telle que definie par le template "Temperature Max"
- platform: template
name: "Triac Temp Ok"
id: temperature_triac_ok
# identification de la periode du jour
- platform: template
name: "Mode- nuit"
id: mode_nuit
# identification du mode heure pleine heure creuse utilisé pour forcer la chauffe la nuit et en heure creuse
- platform: template
name: "Sensor Heure Pleine"
id: mode_hp
# activation du routeur necessite de creer dans home assistant un helper boolean afin d'activer ou non le routage
- platform: homeassistant
name: "Validation Routeur"
entity_id: "input_boolean.inter_validation_routeur"
publish_initial_state: true
id: val_routeur
- platform: gpio
pin:
number: GPIO14
mode: input_pullup
name: validation_affichage
id: val_affichage
on_press:
then:
- binary_sensor.template.publish:
id: backlight
state: ON
- binary_sensor.template.publish:
id: backlight
state: OFF
- platform: template
id: backlight
filters:
- delayed_off: 300s
on_press:
then:
- lambda: |-
id(mydisplay).backlight();
on_release:
then:
- lambda: |-
id(mydisplay).no_backlight();
number:
# Seuil Max sortie triac
- platform: template
name: "Puissance Max"
id: pmax
optimistic: true
restore_value: true
mode: box
min_value: 10
max_value: 80
unit_of_measurement: "%"
step: 5
# Temperature Maxi du triac avant coupure
- platform: template
name: "Temperature Max"
id: tmax
optimistic: true
restore_value: true
initial_value: 40
mode: box
min_value: 0
max_value: 60
unit_of_measurement: "°C"
step: 1
# Coefficient de reactivité du routeur
- platform: template
name: "Coeff Reactivite"
id: coeff_r
optimistic: true
restore_value: true
mode: box
initial_value: 1
min_value: 0
max_value: 10
unit_of_measurement: ""
step: 0.1
text_sensor:
# Puissance Tarifaire en cours
- platform: homeassistant
name: "PTEC"
entity_id: "sensor.ptec"
id: ptec
- platform: wifi_info
ip_address:
name: ESP IP Address
sensor:
- platform: uptime
name: "uptime"
id: esp_uptime
filters:
- lambda: return x / 3600.0;
unit_of_measurement: "hours"
accuracy_decimals: 2
- platform: internal_temperature
name: "Internal Temperature"
id: esp_internal_temperature
- platform: wifi_signal
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: "diagnostic"
- platform: dallas_temp
address: ${dallas_address}
name: "temperature triac"
update_interval: 2s
id: temperature_triac
# filters:
# - filter_out: NAN
# Puissance traversant la sonde 1 / puecs
- platform: modbus_controller
modbus_controller_id: jsymk
id: puissance_ecs
name: "Puissance ECS"
address: 0x004A
unit_of_measurement: "W"
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.0001
register_count: 1
response_size: 4
# Courant traversant la sonde 1 / puecs
- platform: modbus_controller
modbus_controller_id: jsymk
id: courant_ecs
name: "courant ECS"
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
# Facteur de puissance traversant la sonde 1 / puecs
- platform: modbus_controller
modbus_controller_id: jsymk
id: fp_ecs
name: "fp ECS"
address: 0x004C
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.001
register_count: 1
response_size: 4
# Puissance traversant la sonde 2 / Reseau
- platform: modbus_controller
modbus_controller_id: jsymk
id: puissance_reseau_absolue
name: "Puissance Reseau Absolue"
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(sens_pince).state == 1 ) {
id(puissance_reseau).publish_state( id(puissance_reseau_absolue).state *-1);
} else {
id(puissance_reseau).publish_state( id(puissance_reseau_absolue).state );
}
# Facteur de puissance traversant la sonde 2 / reseau
- platform: modbus_controller
modbus_controller_id: jsymk
id: reseau
name: "fp reseau"
address: 0x0054
register_type: holding
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.001
register_count: 1
response_size: 4
# determination si injection ou consomation : sens pince = 1 alors excedent de production sens pince = 0 consommation sur le reseau
- platform: modbus_controller
modbus_controller_id: jsymk
id: sens_pince
name: "Sens_Pince"
address: 0x004E
register_type: holding
value_type: U_DWORD
bitmask: 0X00010000
filters:
- multiply: 1
register_count: 1
response_size: 4
# Affichage Puissance Reseau
- platform: template
name: "Puissance Reseau"
id: puissance_reseau
unit_of_measurement: "W"
state_class: "measurement"
# calcul de l'energie injectée dans le cumulus pour le jour en cours remis a zero a minuit
- platform: total_daily_energy
name: "Energie Injectée ECS"
id: energy_ecs
power_id: puissance_ecs
unit_of_measurement: kWh
device_class: energy
filters:
- multiply: .001
#Affichage valeur increment
- platform: template
name: "Increment"
id: affichage_increment
unit_of_measurement: ""
accuracy_decimals: 2
state_class: "measurement"
# affichage sortie triac
# attention la valeur doit etre divisée par 100 pour affecter la valeur brightness du dimmer
- platform: template
name: "Sortie Triac"
id: affichage_sortie_triac
unit_of_measurement: "%"
state_class: "measurement"
accuracy_decimals: 2
display:
- platform: lcd_pcf8574
id: mydisplay
dimensions: 20x4
address: 0x27
lambda: |-
switch (id(page)){
case 1 :
it.strftime(2, 0, "%H:%M %d-%m-%Y", id(my_time).now());
it.printf(0,1,"tarif:%s", id(mode_hp).state ? "HP" : "HC");
it.printf(16,1,"%s", id(mode_nuit).state ? "Nuit" : "Jour");
it.printf(0,2,"uptime %0.f H",id(esp_uptime).state);
it.printf(14,2,"T=%0.1fc", id(esp_internal_temperature).state);
it.printf(0,3,"Wifi:%0.1f", id(wifi_signal_db).state);
break;
case 2 :
it.strftime(2, 0, "%H:%M %d-%m-%Y", id(my_time).now());
it.printf(0,1,"P Reseau=%0.0fW",id(puissance_reseau).state);
it.printf(0,2,"P ECS=%0.0fW ",id(puissance_ecs).state);
it.printf(0,3,"Energie ECS=%0.1f kWh ",id(energy_ecs).state);
break;
case 3:
it.strftime(2, 0, "%H:%M %d-%m-%Y", id(my_time).now());
it.printf(0,1,"Triac T=%0.1fc", id(temperature_triac).state);
it.printf(10,2,"Ouv T:%0.1f", id(affichage_sortie_triac).state);
it.printf(0,2,"Inc %0.1f", id(affichage_increment).state);
it.printf(0,3,"Mode=%s", id(modeauto).state ? "Auto" : "Manu");
break;
}
switch :
- platform: restart
name: "${friendly_name} Restart"
id: restart_esp
- platform: template
name: "Mode Auto"
id: modeauto
optimistic: true
restore_mode: always_on
output:
- platform: ac_dimmer
id: dimmer_ecs
gate_pin: ${gate_pin}
method: leading
zero_cross_pin:
number: ${zerocross_pin}
mode:
input: true
inverted: yes
min_power: 0.1
- id: led_red
platform: gpio
pin: ${red_led_pin}
# inverted: true
- id: led_green
platform: gpio
pin: ${green_led_pin}
# inverted: true
light:
- platform: monochromatic
name: "Dimmer"
output: dimmer_ecs
id: gradateur
default_transition_length: 50ms
- platform: status_led
id: wifi_status_led
pin:
number: GPIO13
interval:
- interval: 30s
then:
if:
condition:
wifi.connected:
then:
- light.turn_on: wifi_status_led
else:
- light.turn_off: wifi_status_led
- interval: 5s
then:
- script.execute: test_nuit
- script.execute: test_heure_creuse
- lambda: |-
id(page) = (id(page) + 1);
if (id(page) > 3) {
id(page) = 1;
}
- interval: 1s
then:
- script.execute: validation_temperature
- script.execute: calcul_injection
script:
# validation temperature
- id: validation_temperature
mode: single
then:
# Si Temp triac inferieure a tmax - 2 °c alors ok
- if:
condition:
lambda: 'return id(temperature_triac).state < (id(tmax).state-2);'
then:
- binary_sensor.template.publish:
id: temperature_triac_ok
state: ON
- logger.log:
format: "Log temperature triac ok"
level: "info"
- output.turn_off: led_red
# Si Temp Triac supérieur ou égale a tmax alors NOK
- if:
condition:
lambda: 'return id(temperature_triac).state >= id(tmax).state;'
then:
- binary_sensor.template.publish:
id: temperature_triac_ok
state: OFF
- light.turn_off:
id: gradateur
- logger.log:
format: "Log temperature triac nok"
level: "info"
- output.turn_on: led_red
# Si Temp Triac invalide
- if:
condition:
lambda: 'return (isnan(id(temperature_triac).state));'
then:
- binary_sensor.template.publish:
id: temperature_triac_ok
state: OFF
- light.turn_off:
id: gradateur
- logger.log:
format: "Log Temperature triac invalide"
level: "info"
- output.turn_on: led_red
# test nuit
- id: test_nuit
mode: single
then:
- if:
condition:
sun.is_below_horizon
then:
- binary_sensor.template.publish:
id: mode_nuit
state: ON
- logger.log:
format: "Passage Mode Nuit"
level: "info"
- if:
condition:
sun.is_above_horizon
then:
- binary_sensor.template.publish:
id: mode_nuit
state: OFF
- logger.log:
format: "Passage Mode Jour"
level: "info"
# test heure creuse
- id: test_heure_creuse
mode: single
then:
- if:
condition:
lambda: 'return id(ptec).state == "HC..";'
then:
- binary_sensor.template.publish:
id: mode_hp
state: OFF
- logger.log:
format: "Passage Heure Creuse"
level: "info"
- if:
condition:
lambda: 'return id(ptec).state == "HP..";'
then:
- binary_sensor.template.publish:
id: mode_hp
state: ON
- logger.log:
format: "Passage Heure Pleine"
level: "info"
# calcul de la valeur d'injection
- id: calcul_injection
mode: single
then:
# mode jour routage si mode auto activé
- if:
condition:
and:
- binary_sensor.is_on : temperature_triac_ok
- binary_sensor.is_off : mode_nuit
- switch.is_on : modeauto
then:
- lambda: |-
id(increment) = (id(puissance_reseau).state*id(coeff_r).state)/1000*-1;
- lambda: |-
id(sortie_triac) = id(sortie_triac)+id(increment);
if (!isnan(id(sortie_triac))) {
id(sortie_triac) = id(sortie_triac)+id(increment);
}else{
id(sortie_triac)=0;
}
if (id(sortie_triac) <= 0){
id(sortie_triac) = 0;
} else if(id(sortie_triac)>=id(pmax).state){
id(sortie_triac) = id(pmax).state;
}
- light.turn_on:
id: gradateur
brightness: !lambda |-
return id(sortie_triac)/100 ;
- output.turn_on: led_green
- logger.log:
format: "Log Auto OK sortie Triac %f - Increment %f"
args: [ 'id(sortie_triac)', 'id(increment)' ]
level: info
- lambda: |-
id(affichage_sortie_triac).publish_state( id(sortie_triac) );
id(affichage_increment).publish_state( id(increment) );
# mode nuit heure pleine on desactive le gradateur
- if:
condition:
and:
- binary_sensor.is_on: mode_nuit
- binary_sensor.is_on : mode_hp
then:
- lambda: |-
id(sortie_triac) = 0;
id(increment) = 0;
- light.turn_off:
id: gradateur
- output.turn_off: led_green
- logger.log:
format: "Log mode nuit heure pleine"
level: info
# mode nuit heure creuse on active le gradateur
- if:
condition:
and:
- binary_sensor.is_on: mode_nuit
- binary_sensor.is_off : mode_hp
- switch.is_on : modeauto
then:
- lambda: |-
id(sortie_triac) = id(pmax).state;
id(increment) = 0;
- light.turn_on:
id: gradateur
brightness: !lambda |-
return id(sortie_triac)/100 ;
- output.turn_on: led_green
- logger.log:
format: "Log mode nuit heure creuse sortie triac %f"
args: [ 'id(sortie_triac)']
level: info
# il y a un probleme on desactive le gradateur
- if:
condition:
or:
- binary_sensor.is_off : temperature_triac_ok
- switch.is_off: modeauto
then:
- lambda: |-
id(sortie_triac) = 0;
id(increment) = 0;
- light.turn_off:
id: gradateur
- output.turn_off: led_green
- logger.log:
format: "Log mode manuel ou surchauffe"
level: info
- lambda: |-
id(affichage_sortie_triac).publish_state( id(sortie_triac) );
id(affichage_increment).publish_state( id(increment) );
A noter que j'ai complètement détourné ce que faisait REM81 :
- La LED VERTE GPIO32 est allumée quand le dimmer est actif.
- La LED ROUGE GPIO 25 est allumée il y a surchauffe
Le boitier
Je me suis aussi forcé à faire un boitier en impression 3d (perfectible lui aussi, je ne suis pas dessinateur projeteur) ça à le mérite de rentrer dans la boîte.

Les plans sont bien entendu, eux aussi, disponibles sur le github.
Les trous pour les leds ne sont pas fait pour la simple et bonne raison que je vous laisse le choix du positionnement coté connectique ou à l'opposé.
Une fois les tests effectués, fermer le boitier avec de la colle
Rendu de l'installation :

Raccordement
Quand vous intervenez dans votre tableau électrique BIEN COUPER le différentiel d'abonné, aussi appelé disjoncteur principal.

Le routeur va en sortie du disjoncteur de 20A qui sert à protéger le chauffe-eau en remplacement du commutateur jour nuit , comme je n'utilise plus le commutateur HP/HC je l'ai laissé dans mon tableau, mais je l'ai déconnecté et je l'ai remplacé par le routeur.
La sonde de courant doit être installée sur la phase en sortie du disjoncteur général. Attention, elle a un sens.
La valeur du sensor "sens_pince" doit être à 0 quand on est consommateur. Si ce sensor est égal à 1 alors que la production solaire ne couvre pas la totalité de la consommation de la maison, alors retournez la pince.
Le bornier P12 (télé-information) doit être raccordé au compteur linky sur la sortie télé-information.

Les sondes de courant sont à installer sur le fil de phase sortant du disjoncteur du circuit que vous souhaitez mesurer.
Et dans HA alors ?
Le helper
il est indispensable de créer un helper de type boolean
inter_validation_routeur
c'est lui qui définit le mode "vacances"
utility_meter
L'utility_meter qui permettra de définir quelle source d'Energie ( soleil le jour) ou EDF la nuit
utility_meter:
consommation_ecs:
source: sensor.router_tic_4ct_energie_inject_e_ecs
cycle: daily
tariffs:
- nuit
- jour
l'automation associée pour basculer entre la charge "solaire" et le complément EDF la nuit.
alias: peak-offpeak-ecs
description: ""
triggers:
- at: "01:08:00"
variables:
tariff: nuit
trigger: time
- at: "06:08:00"
variables:
tariff: jour
trigger: time
actions:
- target:
entity_id: select.consommation_ecs
data:
option: "{{ tariff }}"
action: select.select_option
Les automations
alias: routeur - mode auto off
description: ""
triggers:
- trigger: time
at: "23:59:00"
conditions:
- condition: or
conditions:
- condition: numeric_state
entity_id: sensor.injection_jour
above: 2000
- condition: state
entity_id: input_boolean.inter_validation_routeur
state: "off"
actions:
- action: switch.turn_off
metadata: {}
data: {}
target:
entity_id:
- switch.router_tic_4ct_mode_auto
- action: notify.pushbullet
metadata: {}
data:
message: mode auto routeur off
mode: single
alias: routeur mode auto on
description: ""
triggers:
- trigger: state
entity_id:
- sun.sun
from: below_horizon
to: above_horizon
conditions:
- condition: state
entity_id: input_boolean.inter_validation_routeur
state: "on"
actions:
- action: switch.turn_on
metadata: {}
data: {}
target:
entity_id:
- switch.router_tic_4ct_mode_auto
- action: notify.pushbullet
metadata: {}
data:
message: mode auto routeur on
mode: single
alias: notification surchauffe routeur
description: ""
triggers:
- entity_id:
- binary_sensor.router_tic_4ct_triac_temp_ok
for:
hours: 0
minutes: 0
seconds: 10
from: "on"
to: "off"
trigger: state
conditions: []
actions:
- data:
message: Surchauffe routeur
action: notify.pushbullet
mode: single
Le dashboard
Le dashboard n'est pas mon fort, mais je vous propose ce que j'ai mis en place.

type: vertical-stack
cards:
- type: horizontal-stack
cards:
- type: gauge
max: 3000
name: "Puissance "
needle: true
entity: sensor.router_tic_4ct_puissance_ecs
- type: entities
entities:
- entity: sensor.consommation_ecs_jour
name: solaire
- entity: sensor.consommation_ecs_nuit
name: EDF
- entity: input_boolean.inter_validation_routeur
name: Validation
title: consommation
- type: custom:apexcharts-card
yaxis:
- id: first
- id: second
opposite: true
stacked: true
graph_span: 7d
update_interval: 15min
span:
start: day
offset: "-6d"
header:
show: true
title: ECS
show_states: true
colorize_states: true
series:
- entity: sensor.consommation_ecs_jour
yaxis_id: first
name: Solaire
color: blue
type: column
group_by:
duration: 1d
func: max
- entity: sensor.consommation_ecs_nuit
yaxis_id: first
color: red
name: EDF
type: column
group_by:
duration: 1d
func: max
- entity: sensor.energy_production_today
yaxis_id: second
name: forecast
color: orange
group_by:
duration: 1d
func: avg
title: ECS

type: custom:plotly-graph
entities:
- entity: sensor.router_tic_4ct_puissance_ecs
- entity: sensor.injection
- entity: sensor.router_tic_4ct_energie_inject_e_ecs
hours_to_show: 2
refresh_interval: 10

type: entities
entities:
- entity: light.router_tic_4ct_dimmer
- entity: switch.router_tic_4ct_mode_auto
- entity: binary_sensor.router_tic_4ct_triac_temp_ok
- entity: binary_sensor.router_tic_4ct_mode_nuit
- entity: sensor.router_tic_4ct_temperature_triac
- entity: sensor.router_tic_4ct_puissance_ecs
- entity: sensor.router_tic_4ct_puissance_reseau
- entity: sensor.router_tic_4ct_sens_pince
- entity: sensor.router_tic_4ct_increment
- entity: number.router_tic_4ct_puissance_max
- entity: number.router_tic_4ct_temperature_max
- entity: number.router_tic_4ct_coeff_reactivite
Remerciements
Je tiens à remercier :
- REM81, @Remy_Crochon, pour m'avoir autorisé à faire du plagiat.
- Charles Hallard, @Hallard, pour avoir démocratisé la télé-information.
- F1ATB pour tous les concepts qu'il a mis en place.
- Le Forum HACF, et les relecteurs des articles
- Le Profes'Solaire avec sa chaine youtube : www.youtube.com/@LeProfesSolaire
- Mais aussi Docteur Cyprien, @tedour pour les tous les tutos didactiques relatif a l'ESP mis en place sur sa chaine YouTube.https://www.youtube.com/@docteurcyprien.
Conclusion
J'espère que vous aurez réussi à réaliser ce routeur solaire, ou en tout cas donné l'envie de le faire. N'hésitez pas à faire vos retours et suggestions.