diff --git a/docs/PROTOCOLE_LVHUB_CERBO.md b/docs/PROTOCOLE_LVHUB_CERBO.md new file mode 100644 index 0000000..4bdba7d --- /dev/null +++ b/docs/PROTOCOLE_LVHUB_CERBO.md @@ -0,0 +1,144 @@ +# Pourquoi le Cerbo ne voit qu'un pack sur deux — protocole de tests + +*Rédigé le 14/09/2026, après l'incident de 16:24.* + +## Le fait à expliquer + +Le LV-Hub reconnaît les deux packs — LED 2 verte et trois bips, qui sont son +accusé de réception. Le Cerbo, relié à ce hub, n'en énumère qu'un. + +``` +nombre_de_modules_en_ligne = 4 (attendu : 9) +capacite_installee = 400 Ah (attendu : 650 = 4 x 100 + 5 x 50) +``` + +## Ce qui est déjà mesuré — à ne pas refaire + +| fait | mesure | +|---|---| +| **Pack 2 porte bien du courant** | 14/09 20:14 : les quatre US5000 débitent −24 A au total, le Cerbo annonce **−41 A** au parc. Les 17 A d'écart sont le pack 2. | +| **Les deux chaînes s'ignorent** | Console sur pack 1 → 4 modules puis `Absent`. Console sur pack 2 → 5 modules puis `Absent`. Aucune n'énumère l'autre. | +| **Deux familles, deux générations** | Pack 1 : US5000, 48V/100AH, firmware **B69.26.0.0**, fabriqués 26-27/03/2026. Pack 2 : US2000C, 48V/50AH, firmware **B69.13.0.0** et **B69.13.2.0**. | +| **Le Cerbo passe par le pack 1** | Débrancher le pack 1 → le Cerbo ne voit plus rien du tout (constaté par le propriétaire). | +| **Pack 2 seul sur le hub a échoué** | Batterie 0 et hub au rouge (constaté par le propriétaire). | +| **Les deux packs sont sains** | 75 cellules du pack 2 à `SOHCount 0` / `Normal`, compteurs 0-1. Pack 1 neuf, aucune alarme. | +| **L'inventaire a changé le 13/09 à 21:35:09** | Événement isolé — ce n'est **pas** un redémarrage de Home Assistant, dont le dernier gros pic date du 06/09 14:21. | + +⚠️ **Le courant mesuré au parc n'est PAS un indicateur d'énumération.** Le Cerbo +lit le shunt, donc tout le parc, y compris ce que son BMS n'énumère pas. Les +seuls critères valables sont `nombre_de_modules_en_ligne` et +`capacite_installee`. + +## Règles de sécurité — tirées de l'incident du 14/09 + +1. **FERMER EDF AVANT TOUTE MANŒUVRE**, et le laisser fermé jusqu'au retour à + l'état initial. Couper la communication du BMS arrête la gestion batterie, + donc l'onduleur, donc la maison : le 14/09 à 16:24 elle est restée dans le + noir **72 secondes**, le temps qu'une automatisation referme le réseau. Avec + EDF déjà fermé, la maison passe en direct et ne voit rien. +2. **De jour, batterie ni pleine ni basse** (SOC 40-80 %). Pleine, on ne peut + rien lire sur la charge ; basse, on n'a pas de marge. +3. **Une manœuvre à la fois**, avec un relevé avant et après. Deux changements + simultanés ne prouvent rien — c'est la leçon du premier test EDF du 27/08, + confondu par un passage nuageux. +4. **Éclairer la baie.** Le propriétaire l'a dit lui-même : l'incident vient + d'une fiche débranchée de nuit. +5. **Retour à l'état initial garanti** à la fin de chaque étape, même si + l'étape échoue. + +Relevé avant et après chaque étape : + + node tools/releve-parc-batterie.mjs "etape N - avant" --console /dev/ttyUSB0 + node tools/releve-parc-batterie.mjs "etape N - apres" --console /dev/ttyUSB0 + +Lecture seule, journal horodaté dans `docs/mesures/`. + +--- + +## Étape 1 — Forcer une ré-énumération, sans toucher au câblage + +**La moins risquée, et à faire en premier.** L'inventaire a changé une fois, le +13/09 à 21:35 : le Cerbo ré-inventorie donc à l'occasion. Peut-être n'a-t-il +simplement jamais rescanné depuis que les deux packs sont en place. + +- Fermer EDF, relevé « avant ». +- Redémarrer le **Cerbo** (interface GX → Paramètres → Général → Redémarrer). + Ne PAS toucher aux modules ni aux câbles. +- Attendre le retour complet, relevé « après ». + +| résultat | conclusion | +|---|---| +| 9 modules / 650 Ah | **c'était un inventaire périmé.** Terminé. | +| toujours 4 / 400 | le Cerbo rescanne et ne retient qu'un groupe → étape 2 | + +Retour arrière : aucun, rien n'a été modifié. + +## Étape 2 — Inverser l'ordre de mise sous tension + +Sur une chaîne Pylontech, l'ordre d'allumage détermine qui prend le rôle de +maître et comment les adresses se distribuent. Si le pack 1 s'annonce toujours +en premier, il est plausible qu'il « prenne la place ». + +- Fermer EDF, relevé « avant ». +- Éteindre les deux packs. Rallumer le **pack 2 d'abord**, attendre sa + stabilisation et les bips du hub, puis le pack 1. +- Relevé « après ». + +| résultat | conclusion | +|---|---| +| 9 modules / 650 Ah | l'ordre était la cause. **À consigner comme procédure de remise en service.** | +| 5 modules / 250 Ah | le Cerbo suit le **premier allumé** — il ne sait pas fusionner les deux groupes → étape 3 | +| toujours 4 / 400 | l'ordre n'y est pour rien → étape 3 | + +Retour arrière : rallumer dans l'ordre d'origine. + +## Étape 3 — Le pack 2 seul, pour savoir si le Cerbo sait lui parler + +Question précise : le Cerbo est-il **capable** de dialoguer avec des US2000C +dans cette installation, ou seulement avec les US5000 ? + +- Fermer EDF, relevé « avant ». +- Déconnecter le pack 1 du hub, laisser le pack 2 seul, comme le pack 1 l'est + aujourd'hui (même type de liaison, même port). +- Relevé « après », puis **tout remettre**. + +| résultat | conclusion | +|---|---| +| 5 modules / 250 Ah | le Cerbo sait parler aux deux, séparément. **Le problème est la COMBINAISON** → étape 4 | +| rien du tout | le pack 2 n'est pas joignable par ce chemin — problème de liaison ou de port, indépendant du mélange | + +⚠️ Sur cette étape la maison tourne sur cinq US2000 seuls, EDF fermé. Courte, +et ne pas la faire en pleine cuisson. + +## Étape 4 — Documentation constructeur, avant toute reconfiguration + +Si l'étape 3 montre que chacun fonctionne seul mais pas ensemble, alors la +question est celle du **mélange US5000 / US2000C sur un même LV-Hub**, et de la +différence de firmware (B69.26 contre B69.13). + +⚠️ **Ne rien reconfigurer avant d'avoir lu la documentation Pylontech sur ce +point précis.** C'est un cas que le constructeur traite explicitement, et je ne +raisonnerai pas de mémoire sur du câblage batterie. Les éléments à chercher, dans +cet ordre : règles de mélange de modèles dans un même groupe et sur un même hub ; +adressage et rôle maître par port du hub ; compatibilité des versions de +firmware entre groupes ; et la procédure officielle de mise en service d'un hub +à deux groupes. + +Une mise à jour de firmware du pack 2 vers la génération du pack 1 est une piste +**possible mais pas démontrée**, et elle se décide sur la documentation, pas sur +une déduction. Si elle est retenue : sauvegarde des relevés avant, un module à +la fois, et jamais pendant que la maison dépend de la batterie. + +--- + +## Ce que le protocole ne cherche PAS à faire + +- **Remettre en cause les 180 A.** Les neuf modules sont sains et le courant se + répartit physiquement sur tous — moins de 14 A par équivalent US2000. Le BMS + garde la main, et il a démontré le 05/09 une réaction en cinq secondes. +- **Réparer quoi que ce soit dans l'urgence.** L'installation fonctionne : le + pack 2 fournit son courant, tous les modules sont sains, aucune alarme. + +Le seul vrai coût de la situation actuelle est que **la protection cellule +visible depuis Home Assistant ne couvre que quatre modules sur neuf**. C'est ce +qui justifie de comprendre, pas de se précipiter. diff --git a/docs/mesures/202609141814-parc-etape-0-reference.log b/docs/mesures/202609141814-parc-etape-0-reference.log new file mode 100644 index 0000000..dd4b54b --- /dev/null +++ b/docs/mesures/202609141814-parc-etape-0-reference.log @@ -0,0 +1,40 @@ +RELEVE PARC BATTERIE — etape 0 - reference +14/09/2026 20:14:45 +======================================================================== + +CE QUE LE CERBO DECLARE + modules en ligne = 4 (depuis 12/09/2026 21:35:09) + capacite installee = 400.0 (depuis 12/09/2026 21:35:09) + charge autorisee = 100.0 (depuis 14/09/2026 16:59:27) + decharge autorisee = 250.0 (depuis 14/09/2026 16:59:27) + tension autorisee = 53.2 (depuis 14/09/2026 16:59:27) + modules bloquant charge = 0 (depuis 12/09/2026 21:20:00) + modules hors ligne = 0 (depuis 12/09/2026 21:13:08) + cellule la plus basse = 3.327 (depuis 12/09/2026 21:35:39) + cellule la plus haute = 3.329 (depuis 12/09/2026 21:35:39) + id cellule haute = 0102 (depuis 12/09/2026 21:35:45) + SOC = 93.0 (depuis 14/09/2026 20:14:44) + tension parc = 49.689998626708984 (depuis 14/09/2026 20:13:48) + courant parc = -41.0 (depuis 14/09/2026 20:14:40) + DVCC appliquee = 30 (depuis 14/09/2026 16:35:15) + contacteur EDF = off (depuis 14/09/2026 17:01:03) + maison = 2035.0 (depuis 14/09/2026 20:14:44) + production = 213.4570030806511 (depuis 14/09/2026 20:14:44) + +ALARMES + aucune + +CE QUE LA CHAINE DECLARE (console sur /dev/ttyUSB0) + modules qui repondent : 4 + 1 49702 -5942 25800 24100 12 25100 0 3313 0 3314 1 Dischg + 2 49699 -6183 25200 23600 12 24500 0 3313 0 3314 2 Dischg + 3 49704 -5980 24800 23200 12 23900 0 3313 0 3314 1 Dischg + 4 49720 -5723 24000 22300 12 23000 0 3314 4 3315 0 Dischg + info 1 US5000 Y260326C50441288 firmware B69.26.0.0 + info 2 US5000 Y260327C50471348 firmware B69.26.0.0 + info 3 US5000 Y260327C50471349 firmware B69.26.0.0 + info 4 US5000 Y260327C50471356 firmware B69.26.0.0 + +======================================================================== +CRITERE DE REUSSITE : 9 modules en ligne et 650 Ah (4 x 100 + 5 x 50). +Tant que le Cerbo annonce 4 modules et 400 Ah, il ne voit qu un groupe. diff --git a/docs/mesures/batteries/202609052040-suivi.csv b/docs/mesures/batteries/202609052040-suivi.csv index 76f6107..4eb262d 100644 --- a/docs/mesures/batteries/202609052040-suivi.csv +++ b/docs/mesures/batteries/202609052040-suivi.csv @@ -3865,3 +3865,17 @@ heure_reelle,etat,courant_mA,c1_mV,c2_mV,c3_mV,c4_mV,c5_mV,c6_mV,c7_mV,c8_mV,c9_ 2026-09-12T18:17:58.953Z,Y230202C30371425,,3327,3328,3328,3327,3326,3328,3327,3327,3327,3324,3328,3328,3328,3327,3328,4 2026-09-12T18:17:59.108Z,Y230202C30371431,,3327,3327,3327,3328,3326,3327,3327,3327,3327,3327,3328,3328,3328,3328,3327,2 2026-09-12T18:17:59.265Z,Y230202C30371427,,3327,3327,3327,3327,3328,3327,3327,3328,3323,3327,3327,3328,3328,3327,3327,5 +2026-09-14T14:31:13.941Z,Y230202C30371426,,3396,3397,3394,3395,3396,3393,3396,3393,3393,3393,3394,3397,3387,3396,3395,10 +2026-09-14T14:31:14.120Z,Y230202C30371425,,3394,3395,3396,3395,3391,3395,3395,3395,3395,3376,3394,3395,3395,3395,3395,20 +2026-09-14T14:31:14.274Z,Y230202C30371431,,3394,3394,3393,3395,3387,3394,3394,3393,3393,3394,3397,3396,3395,3395,3391,10 +2026-09-14T14:31:14.488Z,Y230202C30371427,,3396,3396,3397,3396,3397,3395,3396,3397,3373,3397,3396,3396,3397,3396,3397,24 +2026-09-14T14:41:14.862Z,Y230202C30371425,,3367,3369,3369,3368,3365,3368,3368,3368,3368,3354,3367,3368,3369,3368,3367,15 +2026-09-14T14:41:17.290Z,Y230202C30371427,,3368,3369,3369,3369,3369,3368,3369,3370,3353,3369,3368,3369,3369,3369,3368,17 +2026-09-14T14:51:17.500Z,Y230202C30371426,,3370,3370,3368,3370,3370,3367,3370,3368,3368,3368,3368,3371,3364,3370,3369,7 +2026-09-14T14:51:17.666Z,Y230202C30371425,,3369,3370,3370,3370,3367,3369,3369,3370,3370,3357,3369,3369,3370,3369,3370,13 +2026-09-14T14:51:17.823Z,Y230202C30371431,,3367,3367,3367,3369,3362,3367,3368,3367,3367,3368,3370,3370,3369,3369,3365,8 +2026-09-14T14:51:17.979Z,Y230202C30371427,,3369,3370,3370,3370,3370,3369,3370,3371,3354,3370,3369,3370,3370,3370,3370,17 +2026-09-14T15:01:18.205Z,Y230202C30371426,,3365,3365,3363,3364,3364,3362,3365,3363,3363,3362,3363,3366,3359,3365,3363,7 +2026-09-14T15:01:18.425Z,Y230202C30371425,,3362,3363,3363,3363,3361,3362,3362,3363,3363,3351,3362,3363,3363,3362,3363,12 +2026-09-14T15:01:18.581Z,Y230202C30371431,,3363,3363,3363,3365,3359,3363,3364,3363,3363,3364,3365,3366,3365,3365,3362,7 +2026-09-14T15:01:18.738Z,Y230202C30371427,,3364,3364,3364,3364,3364,3363,3364,3365,3349,3364,3363,3364,3364,3364,3364,16 diff --git a/docs/mesures/batteries/202609141640-console.txt b/docs/mesures/batteries/202609141640-console.txt new file mode 100644 index 0000000..e44dd86 --- /dev/null +++ b/docs/mesures/batteries/202609141640-console.txt @@ -0,0 +1,25 @@ + +===== 14/09/2026 16:40:49 ===== +pwr +pwr + @ +Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St +1 50499 0 29200 26900 27100 3362 3369 Idle Normal Normal Normal 98% 2026-09-14 16:40:57 Normal Normal 28000 Normal +2 50494 0 32900 26500 27000 3354 3368 Idle Normal Normal Normal 97% 2026-09-14 16:40:56 Normal Normal 27600 Normal +3 50485 0 28100 26300 26700 3361 3368 Idle Normal Normal Normal 98% 2026-09-14 16:40:56 Normal Normal 27000 Normal +4 50498 0 32500 25900 26300 3351 3369 Idle Normal Normal Normal 97% 2026-09-14 16:40:56 Normal Normal 26400 Normal +5 50500 0 31800 26000 26600 3355 3370 Idle Normal Normal Normal 97% 2026-09-14 16:40:57 Normal Normal 26700 Normal +6 - - - - - - - Absent - - - - - - - +7 - - - - - - - Absent - - - - - - - +8 - - - - - - - Absent - - - - - - - +9 - - - - - - - Absent - - - - - - - +10 - - - - - - - Absent - - - - - - - +11 - - - - - - - Absent - - - - - - - +12 - - - - - - - Absent - - - - - - - +13 - - - - - - - Absent - - - - - - - +14 - - - - - - - Absent - - - - - - - +15 - - - - - - - Absent - - - - - - - +16 - - - - - - - Absent - - - - - - - + Command completed successfully + $$ + pylon> diff --git a/docs/mesures/batteries/202609141641-console.txt b/docs/mesures/batteries/202609141641-console.txt new file mode 100644 index 0000000..cd00faa --- /dev/null +++ b/docs/mesures/batteries/202609141641-console.txt @@ -0,0 +1,507 @@ + +===== 14/09/2026 16:41:10 ===== +info 1 +info 1 + @ + Device address : 1 + Manufacturer : Pylon + Device name : US2000C + Board version : V10R04 + Board : NF4.E2 + Main Soft version : B69.13.2.0 + Soft version : V1.8 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 22-10-19 + Barcode : Y230202C30371426 + + Specification : 48V/50AH + Cell Number : 15 + Max Dischg Curr : -90000mA + Max Charge Curr : 90000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:12 ===== +info 2 +info 2 + @ + Device address : 2 + Manufacturer : Pylon + Device name : US2000C + Board version : V10R04 + Board : NF4.E2 + Main Soft version : B69.13.2.0 + Soft version : V1.8 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 22-10-19 + Barcode : Y230202C30371425 + + Specification : 48V/50AH + Cell Number : 15 + Max Dischg Curr : -90000mA + Max Charge Curr : 90000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:15 ===== +info 3 + + Device address : 3 + Manufacturer : Pylon + Device name : US2000C + Board version : V10R04 + Board : NF4.E2 + Main Soft version : B69.13.2.0 + Soft version : V1.8 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 22-10-19 + Barcode : Y230202C30371431 + + Specification : 48V/50AH + Cell Number : 15 + Max Dischg Curr : -90000mA + Max Charge Curr : 90000mA + EPONPort rate soh 3 + @ +Power 3 +Battery Voltage SOHCount SOHStatus +0 3366 0 Normal +1 3367 0 Normal +2 3366 0 Normal +3 3369 0 Normal +4 3362 0 Normal +5 3366 0 Normal +6 3367 0 Normal +7 3367 0 Normal +8 3367 0 Normal +9 3367 0 Normal +10 3369 0 Normal +11 3369 0 Normal +12 3368 0 Normal +13 3369 0 Normal +14 3364 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:19 ===== +info 4 +info 4 + @ + Device address : 4 + Manufacturer : Pylon + Device name : US2000C + Board version : V10R04 + Board : NF4.E2 + Main Soft version : B69.13.2.0 + Soft version : V1.8 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 22-10-19 + Barcode : Y230202C30371427 + + Specification : 48V/50AH + Cell Number : 15 + Max Dischg Curr : -90000mA + Max Charge Curr : 90000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:21 ===== +info 5 +info 5 + @ + Device address : 5 + Manufacturer : Pylon + Device name : US2000C + Board version : V10R04 + Board : NF4.E2 + Main Soft version : B69.13.0.0 + Soft version : V1.4 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 22-01-24 + Barcode : K220803C30160323 + + Specification : 48V/50AH + Cell Number : 15 + Max Dischg Curr : -90000mA + Max Charge Curr : 90000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:39 ===== +soh 1 +soh 1 + @ +Power 1 +Battery Voltage SOHCount SOHStatus +0 3368 0 Normal +1 3369 0 Normal +2 3367 0 Normal +3 3368 0 Normal +4 3368 0 Normal +5 3365 0 Normal +6 3368 0 Normal +7 3366 0 Normal +8 3366 0 Normal +9 3366 0 Normal +10 3366 0 Normal +11 3369 0 Normal +12 3362 0 Normal +13 3369 0 Normal +14 3367 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:41 ===== +soh 2 +soh 2 + @ +Power 2 +Battery Voltage SOHCount SOHStatus +0 3367 0 Normal +1 3369 0 Normal +2 3369 0 Normal +3 3369 0 Normal +4 3366 0 Normal +5 3368 0 Normal +6 3368 0 Normal +7 3368 0 Normal +8 3369 0 Normal +9 3356 0 Normal +10 3367 0 Normal +11 3368 0 Normal +12 3369 0 Normal +13 3368 0 Normal +14 3368 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:43 ===== +soh 3 +soh 3 + @ +Power 3 +Battery Voltage SOHCount SOHStatus +0 3367 0 Normal +1 3368 0 Normal +2 3367 0 Normal +3 3369 0 Normal +4 3363 0 Normal +5 3367 0 Normal +6 3368 0 Normal +7 3368 0 Normal +8 3368 0 Normal +9 3369 0 Normal +10 3370 0 Normal +11 3370 0 Normal +12 3369 0 Normal +13 3370 0 Normal +14 3366 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:46 ===== +soh 4 +soh 4 + @ +Power 4 +Battery Voltage SOHCount SOHStatus +0 3368 0 Normal +1 3369 0 Normal +2 3369 0 Normal +3 3368 0 Normal +4 3368 0 Normal +5 3368 0 Normal +6 3369 0 Normal +7 3369 0 Normal +8 3352 0 Normal +9 3368 0 Normal +10 3368 0 Normal +11 3369 0 Normal +12 3369 0 Normal +13 3368 0 Normal +14 3367 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:48 ===== +soh 5 +soh 5 + @ +Power 5 +Battery Voltage SOHCount SOHStatus +0 3367 0 Normal +1 3367 0 Normal +2 3368 0 Normal +3 3368 0 Normal +4 3367 0 Normal +5 3367 0 Normal +6 3367 0 Normal +7 3367 0 Normal +8 3367 0 Normal +9 3354 0 Normal +10 3367 0 Normal +11 3367 0 Normal +12 3367 0 Normal +13 3367 0 Normal +14 3370 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:51 ===== +stat 1 +stat 1 + @ +Device address 1 +Data Items : 23 +HisData Items : 1794 +Charge Cnt. : 0 +Discharge Cnt. : 0 +Charge Times : 37158 +Status Cnt. : 17 +Idle Times : 45507 +COC Times : 0 +COC2 Times : 0 +DOC Times : 0 +DOC2 Times : 0 +COCA Times : 0 +DOCA Times : 0 +SC Times : 0 +Bat OV Times : 1 +Bat HV Times : 1 +Bat LV Times : 0 +Bat UV Times : 0 +Bat SLP Times : 0 +Pwr OV Times : 1 +Pwr HV Times : 1 +Pwr LV Times : 0 +Pwr UV Times : 0 +Pwr SLP Times : 0 +COT Times : 0 +CUT Times : 0 +DOT Times : 0 +DUT Times : 0 +CHT Times : 0 +CLT Times : 0 +DHT Times : 0 +DLT Times : 0 +Shut Times : 59 +Reset Times : 56 +RV Times : 0 +Input OV Times : 0 +SOH Times : 0 +BMICERR Times : 0 +CYCLE Times : 357 +SOH : 97 +Pwr Percent : 98 +Pwr Coulomb : 169663354 +Dsg Cap : 17877354 +HT@0.5C Cnt : 0 +LT@0.5C Cnt : 0 +HT Cnt : 0 +LT Cnt : 0 +LV Cnt : 4925 +LifeWarn Times : 0 +LifeAlarm Times : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:53 ===== +stat 2 +stat 2 + @ +Device address 2 +Data Items : 13 +HisData Items : 1798 +Charge Cnt. : 0 +Discharge Cnt. : 0 +Charge Times : 37634 +Status Cnt. : 19 +Idle Times : 46968 +COC Times : 0 +COC2 Times : 0 +DOC Times : 0 +DOC2 Times : 0 +COCA Times : 0 +DOCA Times : 0 +SC Times : 0 +Bat OV Times : 1 +Bat HV Times : 1 +Bat LV Times : 0 +Bat UV Times : 0 +Bat SLP Times : 0 +Pwr OV Times : 1 +Pwr HV Times : 1 +Pwr LV Times : 0 +Pwr UV Times : 0 +Pwr SLP Times : 0 +COT Times : 0 +CUT Times : 0 +DOT Times : 0 +DUT Times : 0 +CHT Times : 0 +CLT Times : 0 +DHT Times : 0 +DLT Times : 0 +Shut Times : 43 +Reset Times : 62 +RV Times : 0 +Input OV Times : 0 +SOH Times : 7 +BMICERR Times : 0 +CYCLE Times : 353 +SOH : 0 +Pwr Percent : 97 +Pwr Coulomb : 167928750 +Dsg Cap : 17675109 +HT@0.5C Cnt : 0 +LT@0.5C Cnt : 0 +HT Cnt : 0 +LT Cnt : 0 +LV Cnt : 4836 +LifeWarn Times : 0 +LifeAlarm Times : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:55 ===== +stat 3 +stat 3 + @ +Device address 3 +Data Items : 7 +HisData Items : 1794 +Charge Cnt. : 0 +Discharge Cnt. : 0 +Charge Times : 50615 +Status Cnt. : 19 +Idle Times : 60745 +COC Times : 0 +COC2 Times : 0 +DOC Times : 0 +DOC2 Times : 0 +COCA Times : 0 +DOCA Times : 0 +SC Times : 0 +Bat OV Times : 1 +Bat HV Times : 1 +Bat LV Times : 0 +Bat UV Times : 0 +Bat SLP Times : 0 +Pwr OV Times : 1 +Pwr HV Times : 1 +Pwr LV Times : 0 +Pwr UV Times : 0 +Pwr SLP Times : 0 +COT Times : 0 +CUT Times : 0 +DOT Times : 0 +DUT Times : 0 +CHT Times : 0 +CLT Times : 0 +DHT Times : 0 +DLT Times : 0 +Shut Times : 41 +Reset Times : 60 +RV Times : 0 +Input OV Times : 0 +SOH Times : 0 +BMICERR Times : 0 +CYCLE Times : 347 +SOH : 0 +Pwr Percent : 98 +Pwr Coulomb : 169859952 +Dsg Cap : 17362611 +HT@0.5C Cnt : 0 +LT@0.5C Cnt : 0 +HT Cnt : 0 +LT Cnt : 0 +LV Cnt : 4819 +LifeWarn Times : 0 +LifeAlarm Times : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:41:58 ===== +stat 4 +stat 4 + @ +Device address 4 +Data Items : 2 +HisData Items : 1798 +Charge Cnt. : 0 +Discharge Cnt. : 0 +Charge Times : 64722 +Status Cnt. : 21 +Idle Times : 11299 +COC Times : 0 +COC2 Times : 0 +DOC Times : 0 +DOC2 Times : 0 +COCA Times : 0 +DOCA Times : 0 +SC Times : 0 +Bat OV Times : 0 +Bat HV Times : 1 +Bat LV Times : 0 +Bat UV Times : 0 +Bat SLP Times : 0 +Pwr OV Times : 1 +Pwr HV Times : 1 +Pwr LV Times : 0 +Pwr UV Times : 0 +Pwr SLP Times : 0 +COT Times : 0 +CUT Times : 0 +DOT Times : 0 +DUT Times : 0 +CHT Times : 0 +CLT Times : 0 +DHT Times : 0 +DLT Times : 0 +Shut Times : 35 +Reset Times : 53 +RV Times : 0 +Input OV Times : 0 +SOH Times : 12 +BMICERR Times : 0 +CYCLE Times : 347 +SOH : 0 +Pwr Percent : 97 +Pwr Coulomb : 168123078 +Dsg Cap : 17372645 +HT@0.5C Cnt : 0 +LT@0.5C Cnt : 0 +HT Cnt : 0 +LT Cnt : 0 +LV Cnt : 4836 +LifeWarn Times : 0 +LifeAlarm Times : 0 + Command completed successfully + $$ + pylon> diff --git a/docs/mesures/batteries/202609141642-console.txt b/docs/mesures/batteries/202609141642-console.txt new file mode 100644 index 0000000..721ca18 --- /dev/null +++ b/docs/mesures/batteries/202609141642-console.txt @@ -0,0 +1,209 @@ + +===== 14/09/2026 16:42:00 ===== +stat 5 +stat 5 + @ +Device address 5 +Data Items : 0 +HisData Items : 1800 +Charge Cnt. : 0 +Discharge Cnt. : 0 +Charge Times : 33702 +Status Cnt. : 22 +Idle Times : 53520 +COC Times : 0 +COC2 Times : 0 +DOC Times : 0 +DOC2 Times : 0 +COCA Times : 0 +DOCA Times : 0 +SC Times : 0 +Bat OV Times : 0 +Bat HV Times : 0 +Bat LV Times : 0 +Bat UV Times : 0 +Bat SLP Times : 0 +Pwr OV Times : 0 +Pwr HV Times : 0 +Pwr LV Times : 0 +Pwr UV Times : 0 +Pwr SLP Times : 0 +COT Times : 0 +CUT Times : 0 +DOT Times : 0 +DUT Times : 0 +CHT Times : 0 +CLT Times : 0 +DHT Times : 0 +DLT Times : 0 +Shut Times : 11 +Reset Times : 23 +RV Times : 0 +Input OV Times : 0 +SOH Times : 0 +BMICERR Times : 0 +CYCLE Times : 34 +SOH : 0 +Pwr Percent : 97 +Pwr Coulomb : 173360666 +Dsg Cap : 1721220 +HT@0.5C Cnt : 0 +LT@0.5C Cnt : 0 +HT Cnt : 0 +LT Cnt : 0 +LV Cnt : 0 +LifeWarn Times : 0 +LifeAlarm Times : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:42:18 ===== +soh 2 +soh 2 + @ +Power 2 +Battery Voltage SOHCount SOHStatus +0 3367 0 Normal +1 3368 0 Normal +2 3369 0 Normal +3 3368 0 Normal +4 3366 0 Normal +5 3368 0 Normal +6 3368 0 Normal +7 3368 0 Normal +8 3369 0 Normal +9 3356 0 Normal +10 3367 0 Normal +11 3368 0 Normal +12 3369 0 Normal +13 3368 0 Normal +14 3368 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:42:35 ===== +soh 1 +soh 1 + @ +Power 1 +Battery Voltage SOHCount SOHStatus +0 3367 0 Normal +1 3368 0 Normal +2 3367 0 Normal +3 3367 0 Normal +4 3367 0 Normal +5 3364 0 Normal +6 3368 0 Normal +7 3366 0 Normal +8 3366 0 Normal +9 3365 0 Normal +10 3366 0 Normal +11 3368 0 Normal +12 3362 0 Normal +13 3368 0 Normal +14 3366 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:42:38 ===== +soh 2 +soh 2 + @ +Power 2 +Battery Voltage SOHCount SOHStatus +0 3366 0 Normal +1 3368 0 Normal +2 3368 0 Normal +3 3368 0 Normal +4 3365 0 Normal +5 3367 0 Normal +6 3367 0 Normal +7 3368 0 Normal +8 3368 0 Normal +9 3355 0 Normal +10 3366 0 Normal +11 3368 0 Normal +12 3368 0 Normal +13 3367 0 Normal +14 3367 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:42:40 ===== +soh 3 +soh 3 + @ +Power 3 +Battery Voltage SOHCount SOHStatus +0 3367 0 Normal +1 3367 0 Normal +2 3366 0 Normal +3 3368 0 Normal +4 3362 0 Normal +5 3366 0 Normal +6 3368 0 Normal +7 3367 0 Normal +8 3366 0 Normal +9 3367 0 Normal +10 3369 0 Normal +11 3370 0 Normal +12 3368 0 Normal +13 3368 0 Normal +14 3364 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:42:42 ===== +soh 4 +soh 4 + @ +Power 4 +Battery Voltage SOHCount SOHStatus +0 3368 0 Normal +1 3369 0 Normal +2 3369 0 Normal +3 3369 0 Normal +4 3370 0 Normal +5 3368 0 Normal +6 3369 0 Normal +7 3370 0 Normal +8 3353 0 Normal +9 3369 0 Normal +10 3368 0 Normal +11 3369 0 Normal +12 3369 0 Normal +13 3369 0 Normal +14 3369 0 Normal + Command completed successfully + $$ + pylon> + +===== 14/09/2026 16:42:45 ===== +soh 5 +soh 5 + @ +Power 5 +Battery Voltage SOHCount SOHStatus +0 3370 0 Normal +1 3370 0 Normal +2 3370 0 Normal +3 3370 0 Normal +4 3370 0 Normal +5 3370 0 Normal +6 3369 0 Normal +7 3369 0 Normal +8 3370 0 Normal +9 3357 0 Normal +10 3370 0 Normal +11 3370 0 Normal +12 3370 0 Normal +13 3370 0 Normal +14 3373 0 Normal + Command completed successfully + $$ + pylon> diff --git a/docs/mesures/batteries/202609141703-console.txt b/docs/mesures/batteries/202609141703-console.txt new file mode 100644 index 0000000..3ba6edd --- /dev/null +++ b/docs/mesures/batteries/202609141703-console.txt @@ -0,0 +1,25 @@ + +===== 14/09/2026 17:03:58 ===== +pwr +pwr + @ +Power Volt Curr Tempr Tlow Tlow.Id Thigh Thigh.Id Vlow Vlow.Id Vhigh Vhigh.Id Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St SysAlarm.St +1 50500 237 26800 25100 12 26100 0 3364 7 3368 11 Charge Normal Normal Normal 97% 2026-09-14 23:05:45 Normal Normal 26400 Normal Normal +2 50522 211 26300 24500 12 25500 0 3360 11 3370 5 Charge Normal Normal Normal 96% 2026-09-14 23:04:52 Normal Normal 26600 Normal Normal +3 50506 228 25700 24200 12 24900 0 3364 3 3369 14 Charge Normal Normal Normal 97% 2026-09-14 23:05:36 Normal Normal 25800 Normal Normal +4 50520 235 24800 23200 12 23800 0 3365 3 3370 5 Charge Normal Normal Normal 97% 2026-09-14 23:06:12 Normal Normal 24800 Normal Normal +5 - - - - - - - Absent - - - - - - - +6 - - - - - - - Absent - - - - - - - +7 - - - - - - - Absent - - - - - - - +8 - - - - - - - Absent - - - - - - - +9 - - - - - - - Absent - - - - - - - +10 - - - - - - - Absent - - - - - - - +11 - - - - - - - Absent - - - - - - - +12 - - - - - - - Absent - - - - - - - +13 - - - - - - - Absent - - - - - - - +14 - - - - - - - Absent - - - - - - - +15 - - - - - - - Absent - - - - - - - +16 - - - - - - - Absent - - - - - - - + Command completed successfully + $$ + diff --git a/docs/mesures/batteries/202609141704-console.txt b/docs/mesures/batteries/202609141704-console.txt new file mode 100644 index 0000000..1c04d00 --- /dev/null +++ b/docs/mesures/batteries/202609141704-console.txt @@ -0,0 +1,63 @@ + +===== 14/09/2026 17:04:12 ===== +pwr +pwr + @ +Power Volt Curr Tempr Tlow Tlow.Id Thigh Thigh.Id Vlow Vlow.Id Vhigh Vhigh.Id Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St SysAlarm.St +1 50507 314 26800 25100 12 26100 0 3364 7 3368 0 Charge Normal Normal Normal 97% 2026-09-14 23:05:59 Normal Normal 26400 Normal Normal +2 50525 340 26300 24500 12 25500 0 3360 11 3370 1 Charge Normal Normal Normal 96% 2026-09-14 23:05:08 Normal Normal 26600 Normal Normal +3 50508 324 25700 24200 12 24900 0 3364 3 3369 14 Charge Normal Normal Normal 97% 2026-09-14 23:05:52 Normal Normal 25800 Normal Normal +4 50522 369 24800 23200 12 23800 0 3366 3 3370 5 Charge Normal Normal Normal 97% 2026-09-14 23:06:28 Normal Normal 24800 Normal Normal +5 - - - - - - - Absent - - - - - - - +6 - - - - - - - Absent - - - - - - - +7 - - - - - - - Absent - - - - - - - +8 - - - - - - - Absent - - - - - - - +9 - - - - - - - Absent - - - - - - - +10 - - - - - - - Absent - - - - - - - +11 - - - - - - - Absent - - - - - - - +12 - - - - - - - Absent - - - - - - - +13 - - - - - - - Absent - - - - - - - +14 - - - - - - - Absent - - - - - - - +15 - - - - - - - Absent - - - - - - - +16 - - - - - - - Absent - - - - - - - + Command completed successfully + $$ + pylon> + +===== 14/09/2026 17:04:14 ===== +info 1 +info 1 + @ + Device address : 1 + Manufacturer : Pylon + Device name : US5000 + Board version : V10R04 + Board : NF4.E3 + Main Soft version : B69.26.0.0 + Soft version : V2.3 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 25-07-07 + Barcode : Y260326C50441288 + + Device Test Time : 2000-00-00 00:00:00 + Specification : 48V/100AH + Cell Number : 15 + Max Dischg Curr : -105000mA + Max Charge Curr : 105000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Customer Ver.ID : STD + Last Idle Days : 0 + Max Idle Days : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 17:04:16 ===== +info 5 +info 5 + @ +Target device is not present +$$ + pylon> diff --git a/docs/mesures/batteries/202609141705-console.txt b/docs/mesures/batteries/202609141705-console.txt new file mode 100644 index 0000000..1affcb5 --- /dev/null +++ b/docs/mesures/batteries/202609141705-console.txt @@ -0,0 +1,120 @@ + +===== 14/09/2026 17:05:34 ===== +info 1 +info 1 + @ + Device address : 1 + Manufacturer : Pylon + Device name : US5000 + Board version : V10R04 + Board : NF4.E3 + Main Soft version : B69.26.0.0 + Soft version : V2.3 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 25-07-07 + Barcode : Y260326C50441288 + + Device Test Time : 2000-00-00 00:00:00 + Specification : 48V/100AH + Cell Number : 15 + Max Dischg Curr : -105000mA + Max Charge Curr : 105000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Customer Ver.ID : STD + Last Idle Days : 0 + Max Idle Days : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 17:05:36 ===== +info 2 +info 2 + @ + Device address : 2 + Manufacturer : Pylon + Device name : US5000 + Board version : V10R04 + Board : NF4.E3 + Main Soft version : B69.26.0.0 + Soft version : V2.3 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 25-07-07 + Barcode : Y260327C50471348 + + Device Test Time : 2000-00-00 00:00:00 + Specification : 48V/100AH + Cell Number : 15 + Max Dischg Curr : -105000mA + Max Charge Curr : 105000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Customer Ver.ID : STD + Last Idle Days : 0 + Max Idle Days : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 17:05:38 ===== +info 3 +info 3 + @ + Device address : 3 + Manufacturer : Pylon + Device name : US5000 + Board version : V10R04 + Board : NF4.E3 + Main Soft version : B69.26.0.0 + Soft version : V2.3 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 25-07-07 + Barcode : Y260327C50471349 + + Device Test Time : 2000-00-00 00:00:00 + Specification : 48V/100AH + Cell Number : 15 + Max Dischg Curr : -105000mA + Max Charge Curr : 105000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Customer Ver.ID : STD + Last Idle Days : 0 + Max Idle Days : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 17:05:41 ===== +info 4 +info 4 + @ + Device address : 4 + Manufacturer : Pylon + Device name : US5000 + Board version : V10R04 + Board : NF4.E3 + Main Soft version : B69.26.0.0 + Soft version : V2.3 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 25-07-07 + Barcode : Y260327C50471356 + + Device Test Time : 2000-00-00 00:00:00 + Specification : 48V/100AH + Cell Number : 15 + Max Dischg Curr : -105000mA + Max Charge Curr : 105000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Customer Ver.ID : STD + Last Idle Days : 0 + Max Idle Days : 0 + Command completed successfully + $$ + pylon> diff --git a/docs/mesures/batteries/202609142014-console.txt b/docs/mesures/batteries/202609142014-console.txt new file mode 100644 index 0000000..c14ebc5 --- /dev/null +++ b/docs/mesures/batteries/202609142014-console.txt @@ -0,0 +1,153 @@ + +===== 14/09/2026 20:14:47 ===== +pwr +pwr + @ +Power Volt Curr Tempr Tlow Tlow.Id Thigh Thigh.Id Vlow Vlow.Id Vhigh Vhigh.Id Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St SysAlarm.St +1 49702 -5942 25800 24100 12 25100 0 3313 0 3314 1 Dischg Normal Normal Normal 93% 2026-09-15 02:16:35 Normal Normal 25500 Normal Normal +2 49699 -6183 25200 23600 12 24500 0 3313 0 3314 2 Dischg Normal Normal Normal 92% 2026-09-15 02:15:42 Normal Normal 25700 Normal Normal +3 49704 -5980 24800 23200 12 23900 0 3313 0 3314 1 Dischg Normal Normal Normal 93% 2026-09-15 02:16:26 Normal Normal 25000 Normal Normal +4 49720 -5723 24000 22300 12 23000 0 3314 4 3315 0 Dischg Normal Normal Normal 93% 2026-09-15 02:17:02 Normal Normal 24100 Normal Normal +5 - - - - - - - Absent - - - - - - - +6 - - - - - - - Absent - - - - - - - +7 - - - - - - - Absent - - - - - - - +8 - - - - - - - Absent - - - - - - - +9 - - - - - - - Absent - - - - - - - +10 - - - - - - - Absent - - - - - - - +11 - - - - - - - Absent - - - - - - - +12 - - - - - - - Absent - - - - - - - +13 - - - - - - - Absent - - - - - - - +14 - - - - - - - Absent - - - - - - - +15 - - - - - - - Absent - - - - - - - +16 - - - - - - - Absent - - - - - - - + Command completed successfully + $$ + + +===== 14/09/2026 20:14:49 ===== +info 1 +info 1 + @ + Device address : 1 + Manufacturer : Pylon + Device name : US5000 + Board version : V10R04 + Board : NF4.E3 + Main Soft version : B69.26.0.0 + Soft version : V2.3 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 25-07-07 + Barcode : Y260326C50441288 + + Device Test Time : 2000-00-00 00:00:00 + Specification : 48V/100AH + Cell Number : 15 + Max Dischg Curr : -105000mA + Max Charge Curr : 105000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Customer Ver.ID : STD + Last Idle Days : 0 + Max Idle Days : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 20:14:51 ===== +info 2 +info 2 + @ + Device address : 2 + Manufacturer : Pylon + Device name : US5000 + Board version : V10R04 + Board : NF4.E3 + Main Soft version : B69.26.0.0 + Soft version : V2.3 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 25-07-07 + Barcode : Y260327C50471348 + + Device Test Time : 2000-00-00 00:00:00 + Specification : 48V/100AH + Cell Number : 15 + Max Dischg Curr : -105000mA + Max Charge Curr : 105000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Customer Ver.ID : STD + Last Idle Days : 0 + Max Idle Days : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 20:14:53 ===== +info 3 +info 3 + @ + Device address : 3 + Manufacturer : Pylon + Device name : US5000 + Board version : V10R04 + Board : NF4.E3 + Main Soft version : B69.26.0.0 + Soft version : V2.3 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 25-07-07 + Barcode : Y260327C50471349 + + Device Test Time : 2000-00-00 00:00:00 + Specification : 48V/100AH + Cell Number : 15 + Max Dischg Curr : -105000mA + Max Charge Curr : 105000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Customer Ver.ID : STD + Last Idle Days : 0 + Max Idle Days : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 20:14:56 ===== +info 4 +info 4 + @ + Device address : 4 + Manufacturer : Pylon + Device name : US5000 + Board version : V10R04 + Board : NF4.E3 + Main Soft version : B69.26.0.0 + Soft version : V2.3 + Boot version : V1.0 + Comm version : V2.0 + Release Date : 25-07-07 + Barcode : Y260327C50471356 + + Device Test Time : 2000-00-00 00:00:00 + Specification : 48V/100AH + Cell Number : 15 + Max Dischg Curr : -105000mA + Max Charge Curr : 105000mA + EPONPort rate : 1200 + Console Port rate : 115200 + Customer Ver.ID : STD + Last Idle Days : 0 + Max Idle Days : 0 + Command completed successfully + $$ + pylon> + +===== 14/09/2026 20:14:58 ===== +info 5 +info 5 + @ +Target device is not present +$$ + pylon> diff --git a/tests/golden-acm/scenarios.mjs b/tests/golden-acm/scenarios.mjs index 8dd4141..fe44920 100644 --- a/tests/golden-acm/scenarios.mjs +++ b/tests/golden-acm/scenarios.mjs @@ -147,10 +147,24 @@ export const scenarios = [ // Le 130 A venait de l'ANCIEN parc, sorti le 12/09. 150 A est un palier, // pas une cible : on regarde ce qu'il libere avant d'aller plus loin. id: 'acm-jour-boost', - label: 'Jour, SOC 60 sous 80 : capture PV maximale, 150 A a 51,5 V (releve du 13/09 : la batterie butait sur 130)', + // 14/09/2026 — 150 -> 180 A. Neuf releves consecutifs a 11:45, pas de 12 s : + // courant 148-150 A COLLE au plafond de 150, tension 50,38-50,41 V pour une + // consigne de 51,5 (donc 1,1 V de marge, aucune absorption : ce n'est PAS la + // tension qui limite), ecart entre cellules 2 mV, zero alarme, et 4 a 6 + // chargeurs solaires sur 6 qui declarent se retenir. Le bilan bouclait au + // watt pres — batterie 7,56 kW + maison 0,75 pour 8,4 kW produits, signature + // de l'ecretage. C'etait donc NOTRE plafond, pas le materiel. + // + // ⚠️ Le parc n'est PAS celui que le Cerbo declare : quatre US5000 et cinq + // US2000, soit TREIZE equivalents 2000, dont le Cerbo ne voit que les quatre + // US5000 (d'ou « 4 modules » et « 400 Ah »). Les 250 A qu'il autorise sont + // calcules sur quatre modules et s'appliquent a treize equivalents : a 180 A + // cela fait moins de 14 A par equivalent. Corrobore par la mesure d'energie, + // 0,312 kWh par point de SOC quand treize equivalents en predisent 0,325. + label: 'Jour, SOC 60 sous 80 : capture PV maximale, 180 A a 51,5 V (releve du 14/09 : la batterie butait sur 150)', time: h(ETE, '11:00'), overrides: { ...parcNeuf, soc_percent: '60', pv_power_w: '4000' }, - expect: { mode: 'day_pv_boost', profil: 'pv_boost', courantA: 150, tensionV: 51.5 }, + expect: { mode: 'day_pv_boost', profil: 'pv_boost', courantA: 180, tensionV: 51.5 }, }, { id: 'acm-jour-mid', @@ -158,10 +172,13 @@ export const scenarios = [ // l'ANCIEN parc. A 12:49, SOC 82, la batterie prenait encore 127 A juste // avant que l'ACM ne la ramene a son plafond. La table reste decroissante : // 150 / 130 / 80 / 30. - label: 'Jour, SOC 85 entre 80 et 90 : 130 A a 51,5 V', + // 14/09/2026 — 130 -> 160 A, dans le meme mouvement que pv_boost : la table + // doit rester DECROISSANTE, un palier haut ne peut pas depasser celui d'en + // dessous. Table : 180 / 160 / 80 / 30. + label: 'Jour, SOC 85 entre 80 et 90 : 160 A a 51,5 V', time: h(ETE, '11:00'), overrides: { ...parcNeuf, soc_percent: '85', pv_power_w: '4000', battery_voltage_v: '50.5' }, - expect: { mode: 'day_pv_mid', profil: 'pv_mid', courantA: 130, tensionV: 51.5 }, + expect: { mode: 'day_pv_mid', profil: 'pv_mid', courantA: 160, tensionV: 51.5 }, }, { id: 'acm-jour-topoff-52v4', @@ -202,7 +219,7 @@ export const scenarios = [ overrides: { ...parcNeuf, soc_percent: '60', override_enabled: 'on', override_profile: 'manual', override_current_a: '12', override_voltage_v: '50.0', override_until: '2026-07-15 09:00:00' }, - expect: { overrideActif: false, mode: 'day_pv_boost', courantA: 150 }, + expect: { overrideActif: false, mode: 'day_pv_boost', courantA: 180 }, }, // ------------------------------------------------------------ publication diff --git a/tools/patch-20260914-plafond-180a.mjs b/tools/patch-20260914-plafond-180a.mjs new file mode 100644 index 0000000..ddae834 --- /dev/null +++ b/tools/patch-20260914-plafond-180a.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node +// Releve le plafond de courant de charge : pv_boost 150 -> 180 A, pv_mid 130 -> 160 A. +// +// MESURE DU 14/09/2026 11:45, neuf releves consecutifs a douze secondes : +// courant 148-150 A, colle au plafond de 150, jamais en dessous de 148 +// tension 50,38-50,41 V pour une consigne de 51,5 V — 1,1 V de marge, donc +// AUCUNE phase d absorption : ce n est pas la tension qui limite +// cellules ecart 2 mV, aucune alarme, zero module bloquant la charge +// solaire 4 a 6 chargeurs sur 6 declarent se retenir, en continu +// bilan batterie 7,56 kW + maison 0,75 = 8,31 kW pour 8,4 kW produits +// — la production se cale sur ce que le systeme absorbe : ecretage +// +// C est donc NOTRE plafond qui limite, pas le materiel. +// +// ⚠️ LE PARC N EST PAS CELUI QUE LE CERBO DECLARE. Il compte quatre US5000 et +// cinq US2000, soit TREIZE equivalents US2000 ; le Cerbo ne voit que les quatre +// US5000 (d ou `nombre_de_modules_en_ligne` = 4 et `capacite_installee` = +// 400 Ah). Les 250 A qu il autorise sont donc calcules sur quatre modules et +// s appliquent a treize equivalents. A 180 A cela fait moins de 14 A par +// equivalent 2000. Corrobore par la mesure d energie : 0,312 kWh par point de +// SOC, quand treize equivalents (~650 Ah a 50 V) en predisent 0,325. +// +// Le haut de charge n est PAS touche : au-dela de 90 % c est la tension de +// cellule qui devient le facteur limitant, et le BMS l arbitre bien mieux qu un +// palier de courant — il a demontre le 05/09 une reaction en CINQ SECONDES. +import fs from 'fs'; + +const cible = process.argv[2] || 'workflows/victron-auto-charge-manager-v1.2.json'; +const wf = JSON.parse(fs.readFileSync(cible, 'utf8')); +const node = wf.nodes.find((n) => n.name === 'Compute Charge Targets'); +if (!node) { console.error('noeud « Compute Charge Targets » introuvable'); process.exit(1); } + +let code = node.parameters.jsCode; +const remplacements = [ + // Les deux plafonds de la table. + ['pv_boost 150 -> 180 A', /(pv_boost:\s*\{\s*current_a:\s*)150/, '$1180'], + ['pv_mid 130 -> 160 A', /(pv_mid:\s*\{\s*current_a:\s*)130/, '$1160'], + + // ⚠️ ET LE RELEVEMENT ANTI-BRIDAGE, QUI EST MORT — constat, pas correctif. + // + // Le node porte un mecanisme cense detecter EXACTEMENT la situation du + // 14/09 : « courant batterie a moins de 3 A du plafond, PV au-dessus de + // 6 kW, SOC sous 95 » — et relever alors le plafond. La condition a bien ete + // remplie ce matin, pendant une heure. Elle n'a rien releve. + // + // Parce que son relevement vaut `Math.max(targetCurrentA, soc >= 90 ? 80 : + // 130)` et que le plafond du profil est deja au-dessus de ce plancher dans + // TOUTES les bandes : + // + // SOC 61 pv_boost 180 vs plancher 130 -> max = 180, inchange + // SOC 85 pv_mid 160 vs plancher 130 -> max = 160, inchange + // SOC 92 pv_topoff 80 vs plancher 80 -> max = 80, inchange + // + // On remplace la constante par la valeur de la table dont elle a pris du + // retard — un lecteur ne croira plus a un plancher a 130 A qui n'existe pas. + // ⚠️ MAIS CELA NE LE RANIME PAS : le mecanisme reste un no-op dans les deux + // formes, et c'est VOULU de ne pas le reveiller ici. Un plafond de securite + // qui se releve tout seul parce qu'on bute dessus est precisement la forme + // de boucle qui a produit six oscillateurs dans ce projet. Le ressusciter + // demande sa propre reflexion, ses propres scenarios, et une decision du + // proprietaire — pas un effet de bord d'un changement de palier. + ['plancher anti-bridage : constante perimee -> valeur de la table', + /targetCurrentA = Math\.max\(targetCurrentA, soc >= 90 \? 80 : 130\);/, + `targetCurrentA = Math.max(targetCurrentA, + // Valeurs de la table, et non des constantes qui prennent du retard. + // ⚠️ Ce Math.max est un NO-OP : le plafond du profil est au-dessus du + // plancher dans toutes les bandes. Constate le 14/09/2026 ; le + // mecanisme n'a pas ete ranime deliberement (un plafond qui se releve + // parce qu'on bute dessus est une boucle de retroaction). + soc >= 90 ? cfg.profiles.pv_topoff.current_a : cfg.profiles.pv_mid.current_a);`], +]; +let n = 0; +for (const [quoi, motif, par] of remplacements) { + if (!motif.test(code)) { console.error(`motif introuvable pour ${quoi} — deja applique ?`); process.exit(1); } + code = code.replace(motif, par); n++; +} +node.parameters.jsCode = code; +fs.writeFileSync(cible, JSON.stringify(wf, null, 2) + '\n'); +console.log(`${n} plafond(s) releve(s) dans ${cible}`); diff --git a/tools/releve-parc-batterie.mjs b/tools/releve-parc-batterie.mjs new file mode 100644 index 0000000..c7192a4 --- /dev/null +++ b/tools/releve-parc-batterie.mjs @@ -0,0 +1,107 @@ +#!/usr/bin/env node +// RELEVE DE L ETAT DU PARC BATTERIE — LECTURE SEULE, rejouable. +// +// node tools/releve-parc-batterie.mjs "etape 0 - reference" +// node tools/releve-parc-batterie.mjs "etape 1 - apres reboot cerbo" --console /dev/ttyUSB0 +// +// Prend une photo complete de ce que le Cerbo voit du parc, et si une console +// serie est branchee, de ce que la chaine declare de son cote. Ecrit un journal +// horodate dans docs/mesures/ pour que deux etapes se comparent ligne a ligne. +// +// AUCUNE ECRITURE vers Home Assistant, vers le Cerbo ou vers les modules. +import fs from 'fs'; +import { execFileSync } from 'child_process'; + +const etiquette = process.argv[2] || 'releve'; +const port = process.argv.includes('--console') + ? process.argv[process.argv.indexOf('--console') + 1] : null; + +const env = fs.readFileSync('.env', 'utf8'); +const jeton = (env.match(/^HA_TOKEN=(.*)$/m)?.[1] || '').trim().replace(/^["']|["']$/g, ''); +const HA = 'http://10.10.10.17:8123'; + +// Ce que le CERBO declare du parc. Les deux premieres lignes sont le critere de +// reussite du protocole : 9 modules et 650 Ah (4 x 100 + 5 x 50) diraient qu il +// voit enfin les deux groupes. +const CERBO = [ + ['sensor.airtria_8_us2000c_nombre_de_modules_en_ligne', 'modules en ligne'], + ['sensor.airtria_8_us2000c_capacite_installee', 'capacite installee'], + ['sensor.airtria_8_us2000c_maximum_allowed_charge_current', 'charge autorisee'], + ['sensor.airtria_8_us2000c_maximum_allowed_discharge_current', 'decharge autorisee'], + ['sensor.airtria_8_us2000c_maximum_allowed_charging_voltage', 'tension autorisee'], + ['sensor.airtria_8_us2000c_nombre_de_modules_bloquant_la_charge', 'modules bloquant charge'], + ['sensor.airtria_8_us2000c_nombre_de_modules_hors_ligne', 'modules hors ligne'], + ['sensor.airtria_8_us2000c_tension_minimale_de_la_cellule', 'cellule la plus basse'], + ['sensor.airtria_8_us2000c_tension_maximale_de_la_cellule', 'cellule la plus haute'], + ['sensor.airtria_8_us2000c_id_de_la_cellule_a_tension_maximale', 'id cellule haute'], + ['sensor.victron_battery_soc', 'SOC'], + ['sensor.victron_battery_voltage', 'tension parc'], + ['sensor.victron_battery_current', 'courant parc'], + ['sensor.victron_max_charge_current_setting', 'DVCC appliquee'], + ['switch.edf_general_ok_switch', 'contacteur EDF'], + ['sensor.victron_consumption_power_l1', 'maison'], + ['sensor.victron_pv_power_system', 'production'], +]; + +const etat = async (id) => { + try { + const r = await fetch(`${HA}/api/states/${id}`, { headers: { Authorization: `Bearer ${jeton}` } }); + if (!r.ok) return null; + const s = await r.json(); + return { v: s.state, depuis: new Date(s.last_changed).toLocaleString('fr-FR', { timeZone: 'Europe/Paris' }) }; + } catch { return null; } +}; + +const lignes = []; +const dire = (s) => { lignes.push(s); console.log(s); }; + +const maintenant = new Date(); +dire(`RELEVE PARC BATTERIE — ${etiquette}`); +dire(maintenant.toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })); +dire('='.repeat(72)); + +dire('\nCE QUE LE CERBO DECLARE'); +for (const [id, libelle] of CERBO) { + const e = await etat(id); + dire(' ' + libelle.padEnd(26) + '= ' + String(e ? e.v : '(absent)').padEnd(22) + (e ? '(depuis ' + e.depuis + ')' : '')); +} + +dire('\nALARMES'); +try { + const tout = await (await fetch(`${HA}/api/states`, { headers: { Authorization: `Bearer ${jeton}` } })).json(); + const mauvais = tout.filter((x) => /alarm|alarme|error|erreur|bloquant|hors_ligne/i.test(x.entity_id) + && !/^(no_alarm|no_error|off|0|0\.0|unavailable|unknown|normal|ok)$/i.test(String(x.state))); + if (!mauvais.length) dire(' aucune'); + for (const x of mauvais.slice(0, 14)) dire(' ' + x.entity_id.slice(0, 58).padEnd(60) + '= ' + x.state); +} catch (e) { dire(' lecture impossible : ' + e.message); } + +if (port) { + dire(`\nCE QUE LA CHAINE DECLARE (console sur ${port})`); + for (const cmd of ['pwr', 'info 1', 'info 2', 'info 3', 'info 4', 'info 5']) { + try { + const out = execFileSync('node', ['tools/pylontech-console.mjs', '--port', port, '--commande', cmd], + { encoding: 'utf8', timeout: 45000 }); + if (cmd === 'pwr') { + const l = out.split('\n').filter((x) => /^\d+\s/.test(x)); + const presents = l.filter((x) => !/Absent/.test(x)); + dire(` modules qui repondent : ${presents.length}`); + for (const x of presents) dire(' ' + x.trim().slice(0, 100)); + } else { + const b = out.match(/Barcode\s*:\s*(\S+)/)?.[1]; + const d = out.match(/Device name\s*:\s*(\S+)/)?.[1]; + const f = out.match(/Main Soft version\s*:\s*(\S+)/)?.[1]; + if (b) dire(` ${cmd.padEnd(8)} ${String(d).padEnd(9)} ${b} firmware ${f}`); + } + } catch { if (cmd === 'pwr') dire(' console injoignable'); } + } +} else { + dire('\n(pas de console branchee — passer --console /dev/ttyUSB0 pour lire la chaine)'); +} + +dire('\n' + '='.repeat(72)); +dire('CRITERE DE REUSSITE : 9 modules en ligne et 650 Ah (4 x 100 + 5 x 50).'); +dire('Tant que le Cerbo annonce 4 modules et 400 Ah, il ne voit qu un groupe.'); + +const nom = `docs/mesures/${maintenant.toISOString().slice(0, 16).replace(/[-:T]/g, '')}-parc-${etiquette.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.log`; +fs.writeFileSync(nom, lignes.join('\n') + '\n'); +console.log(`\n--- journalise dans ${nom}`); diff --git a/workflows/victron-auto-charge-manager-v1.2.json b/workflows/victron-auto-charge-manager-v1.2.json index 3d7df9a..1bafce2 100644 --- a/workflows/victron-auto-charge-manager-v1.2.json +++ b/workflows/victron-auto-charge-manager-v1.2.json @@ -73,7 +73,7 @@ }, { "parameters": { - "jsCode": "function num(v, fallback = NaN) {\n const n = Number.parseFloat(v);\n return Number.isFinite(n) ? n : fallback;\n}\n\nfunction low(v) {\n return (v ?? '').toString().trim().toLowerCase();\n}\n\nfunction parseMaybeJson(v) {\n if (typeof v !== 'string') return v;\n try {\n return JSON.parse(v);\n } catch {\n return v;\n }\n}\n\nfunction extractSnapshot(input) {\n if (typeof input === 'string') return parseMaybeJson(input);\n if (input?.data && typeof input.data === 'string') return parseMaybeJson(input.data);\n if (input?.body && typeof input.body === 'string') return parseMaybeJson(input.body);\n if (input?.data && typeof input.data === 'object') return input.data;\n if (input?.body && typeof input.body === 'object') return input.body;\n return input;\n}\n\nfunction round1(v) {\n return Math.round(v * 10) / 10;\n}\n\nfunction timeToMinutes(hh, mm) {\n return hh * 60 + mm;\n}\n\nfunction inRange(minNow, startMin, endMin) {\n return minNow >= startMin && minNow < endMin;\n}\n\nfunction getParisNowParts() {\n const fmt = new Intl.DateTimeFormat('fr-FR', {\n timeZone: 'Europe/Paris',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false,\n });\n\n const parts = Object.fromEntries(\n fmt.formatToParts(new Date())\n .filter(p => p.type !== 'literal')\n .map(p => [p.type, p.value])\n );\n\n const date = `${parts.year}-${parts.month}-${parts.day}`;\n const localStamp = `${date} ${parts.hour}:${parts.minute}:${parts.second}`;\n\n return {\n year: Number(parts.year),\n month: Number(parts.month),\n day: Number(parts.day),\n hour: Number(parts.hour),\n minute: Number(parts.minute),\n second: Number(parts.second),\n date,\n localStamp,\n minNow: Number(parts.hour) * 60 + Number(parts.minute),\n };\n}\n\nfunction forecastBand(kwh) {\n if (!Number.isFinite(kwh)) return 'unknown';\n if (kwh >= 25) return 'sunny';\n if (kwh >= 12) return 'mixed';\n return 'dark';\n}\n\nconst cfg = {\n winterMonths: [11, 12, 1, 2],\n\n windows: {\n nightHC_start: timeToMinutes(1, 24),\n nightHC_end: timeToMinutes(7, 24),\n\n middayHC_start: timeToMinutes(12, 24),\n middayHC_end: timeToMinutes(14, 24),\n\n winterDay_start: timeToMinutes(7, 15),\n winterDay_end: timeToMinutes(16, 45),\n\n summerDay_start: timeToMinutes(7, 30),\n summerDay_end: timeToMinutes(19, 30),\n },\n\n profiles: {\n night_slow: { current_a: 30, voltage_v: 50.1 },\n takeover_hold: { current_a: 2, voltage_v: 50.1 },\n pv_boost: { current_a: 150, voltage_v: 51.5 },\n pv_mid: { current_a: 130, voltage_v: 51.5 },\n pv_topoff: { current_a: 80, voltage_v: 52.4 },\n pv_hold: { current_a: 30, voltage_v: 50.6 },\n midday_grid_catchup: { current_a: 30, voltage_v: 50.8 },\n emergency_rescue: { current_a: 60, voltage_v: 50.4 },\n },\n\n voltagePublishDelta: 0.05,\n currentPublishDelta: 0.5,\n\n mobileNotifyDomain: 'notify',\n mobileNotifyService: 'mobile_app_iphone_16_de_tt'\n};\n\nconst MQTT_TOPIC_CHARGE_CURRENT = 'W/c0619ab34ac5/settings/0/Settings/SystemSetup/MaxChargeCurrent';\nconst MQTT_TOPIC_CHARGE_VOLTAGE = 'W/c0619ab34ac5/settings/0/Settings/SystemSetup/MaxChargeVoltage';\n\nconst raw = $input.first().json;\nconst input = extractSnapshot(raw);\nconst now = getParisNowParts();\n\nconst soc = num(input.soc_percent, 0);\nconst batteryVoltageV = num(input.battery_voltage_v, NaN);\nconst batteryCurrentA = num(input.battery_current_a, NaN);\nconst pvPowerW = num(input.pv_power_w, 0);\nconst edfSwitch = low(input.edf_switch || 'off') === 'on' ? 'on' : 'off';\n\nconst todayRemainingKwh = num(input.today_remaining_kwh, 0);\nconst tomorrowKwh = num(input.tomorrow_kwh, 0);\n\nconst currentMaxChargeCurrentA = num(input.current_max_charge_current_a, 0);\nconst currentMaxChargeVoltageV = num(input.current_max_charge_voltage_v, 0);\n\nconst autoEnabled = low(input.auto_enabled || 'off') === 'on';\nconst nightTakeover = low(input.night_takeover || 'off') === 'on';\nconst nightTargetSoc = num(input.night_target_soc, 35);\nconst notificationsEnabled = low(input.notifications_enabled || 'off') === 'on';\nconst lastSignature = (input.last_signature ?? '').toString().trim();\n\nconst overrideEnabled = low(input.override_enabled || 'off') === 'on';\nconst overrideProfile = low(input.override_profile || 'auto');\nconst overrideCurrentA = num(input.override_current_a, NaN);\nconst overrideVoltageV = num(input.override_voltage_v, NaN);\nconst overrideUntil = (input.override_until ?? '').toString().trim();\nconst overrideReason = (input.override_reason ?? '').toString().trim();\n\nconst isWinter = cfg.winterMonths.includes(now.month);\nconst inNightHC = inRange(now.minNow, cfg.windows.nightHC_start, cfg.windows.nightHC_end);\nconst inMiddayHC = inRange(now.minNow, cfg.windows.middayHC_start, cfg.windows.middayHC_end);\nconst inDayWindow = isWinter\n ? inRange(now.minNow, cfg.windows.winterDay_start, cfg.windows.winterDay_end)\n : inRange(now.minNow, cfg.windows.summerDay_start, cfg.windows.summerDay_end);\n\nconst forecastRefKwh = now.hour < 8 ? Math.max(todayRemainingKwh, tomorrowKwh, 0) : todayRemainingKwh;\nconst forecastState = forecastBand(forecastRefKwh);\n\nfunction profileValues(profileName) {\n if (profileName === 'manual') {\n return {\n current_a: Number.isFinite(overrideCurrentA) ? overrideCurrentA : cfg.profiles.pv_boost.current_a,\n voltage_v: Number.isFinite(overrideVoltageV) ? overrideVoltageV : cfg.profiles.pv_boost.voltage_v,\n };\n }\n return cfg.profiles[profileName] || null;\n}\n\nconst overrideActive =\n overrideEnabled &&\n overrideProfile !== 'auto' &&\n /^\\d{4}-\\d{2}-\\d{2}/.test(overrideUntil) &&\n now.localStamp < overrideUntil;\n\nlet mode = 'idle';\nlet effectiveProfile = 'auto_idle';\nlet targetCurrentA = currentMaxChargeCurrentA || 30;\nlet targetVoltageV = currentMaxChargeVoltageV || 50.5;\nlet criticalAlert = false;\nlet reason = [];\n\nif (!autoEnabled) {\n mode = 'automation_disabled';\n effectiveProfile = 'manual_keep_current';\n targetCurrentA = currentMaxChargeCurrentA || 30;\n targetVoltageV = currentMaxChargeVoltageV || 50.5;\n reason.push(\"Automatisation désactivée, conservation des réglages actuels\");\n} else {\n if (soc < 20 && !inNightHC && !inMiddayHC) {\n mode = 'emergency_rescue';\n effectiveProfile = 'emergency_rescue';\n targetCurrentA = cfg.profiles.emergency_rescue.current_a;\n targetVoltageV = cfg.profiles.emergency_rescue.voltage_v;\n criticalAlert = true;\n reason.push(`SOC critique ${soc}%`);\n }\n\n // P4 : takeover EVCS nocturne. EDF est ON pour la maison + le VE (HC),\n // la batterie doit seulement être RAMENÉE à la cible matin puis MAINTENUE,\n // pas rechargée à fond (cible adaptée au forecast : été ~35%, hiver ~85%).\n else if (nightTakeover && edfSwitch === 'on') {\n const holdTargetSoc = Math.min(95, Math.max(20, nightTargetSoc));\n if (soc < holdTargetSoc - 1) {\n mode = 'evcs_takeover_recover';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(`Takeover EVCS : recharge vers cible matin ${holdTargetSoc}% (SOC ${soc}%)`);\n } else {\n mode = 'evcs_takeover_hold';\n effectiveProfile = 'takeover_hold';\n targetCurrentA = cfg.profiles.takeover_hold.current_a;\n targetVoltageV = cfg.profiles.takeover_hold.voltage_v;\n reason.push(`Takeover EVCS : maintien SOC ~${holdTargetSoc}% (maison+VE sur HC, batterie en pause)`);\n }\n }\n\n else if (isWinter && inNightHC) {\n let targetSoc;\n if (forecastState === 'sunny') targetSoc = 40;\n else if (forecastState === 'mixed') targetSoc = 60;\n else targetSoc = 80;\n\n if (soc < targetSoc - 1) {\n mode = 'winter_night_charge_window';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(`Hiver HC nuit, cible SOC ${targetSoc}% selon forecast ${forecastState}`);\n } else {\n mode = 'winter_night_idle';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(`Hiver HC nuit, SOC ${soc}% déjà suffisant`);\n }\n }\n\n else if (!isWinter && inNightHC) {\n if (soc < 22) {\n mode = 'summer_night_rescue_window';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(\"Été HC nuit, secours SOC < 22%\");\n } else {\n mode = 'summer_night_idle';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(\"Été HC nuit, pas de besoin réseau\");\n }\n }\n\n else if (\n inMiddayHC &&\n soc < 85 &&\n forecastRefKwh < 8 &&\n pvPowerW < 1500\n ) {\n mode = 'midday_catchup_window';\n effectiveProfile = 'midday_grid_catchup';\n targetCurrentA = cfg.profiles.midday_grid_catchup.current_a;\n targetVoltageV = cfg.profiles.midday_grid_catchup.voltage_v;\n reason.push(`HC midi rattrapage : SOC ${soc}% | forecast ${forecastRefKwh} kWh | PV ${pvPowerW} W`);\n }\n\n else if (inDayWindow) {\n if (soc < 80) {\n mode = 'day_pv_boost';\n effectiveProfile = 'pv_boost';\n targetCurrentA = cfg.profiles.pv_boost.current_a;\n targetVoltageV = cfg.profiles.pv_boost.voltage_v;\n reason.push(`Jour SOC bas ${soc}% : capture PV max`);\n } else if (soc < 90) {\n mode = 'day_pv_mid';\n effectiveProfile = 'pv_mid';\n targetCurrentA = cfg.profiles.pv_mid.current_a;\n targetVoltageV = cfg.profiles.pv_mid.voltage_v;\n reason.push(`Jour SOC intermédiaire ${soc}%`);\n } else if (soc < 95) {\n mode = 'day_pv_topoff';\n effectiveProfile = 'pv_topoff';\n targetCurrentA = cfg.profiles.pv_topoff.current_a;\n targetVoltageV = cfg.profiles.pv_topoff.voltage_v;\n reason.push(`Jour top-off vers 95% | SOC ${soc}%`);\n } else {\n mode = 'day_pv_hold';\n effectiveProfile = 'pv_hold';\n targetCurrentA = cfg.profiles.pv_hold.current_a;\n targetVoltageV = cfg.profiles.pv_hold.voltage_v;\n reason.push(`Jour maintien haut SOC ${soc}%`);\n }\n\n if (\n Number.isFinite(batteryVoltageV) &&\n soc < 95 &&\n pvPowerW > 2000 &&\n batteryVoltageV >= (targetVoltageV - 0.15)\n ) {\n targetVoltageV = Math.max(targetVoltageV, 51.3);\n targetCurrentA = Math.max(targetCurrentA, 80);\n if (soc >= 90) {\n effectiveProfile = 'pv_topoff';\n mode = 'day_pv_topoff';\n } else {\n effectiveProfile = 'pv_boost';\n }\n reason.push(`Batterie proche consigne (${round1(batteryVoltageV)}V), relèvement anti-bridage`);\n }\n\n if (\n Number.isFinite(batteryCurrentA) &&\n soc < 95 &&\n pvPowerW > 6000 &&\n batteryCurrentA >= (targetCurrentA - 3)\n ) {\n targetCurrentA = Math.max(targetCurrentA, soc >= 90 ? 80 : 130);\n reason.push(`Courant batterie proche limite (${round1(batteryCurrentA)}A), relèvement du plafond`);\n }\n }\n\n\n else {\n mode = 'off_window_idle';\n effectiveProfile = 'pv_hold';\n targetCurrentA = cfg.profiles.pv_hold.current_a;\n targetVoltageV = cfg.profiles.pv_hold.voltage_v;\n reason.push(\"Hors fenêtre utile\");\n }\n}\n\nif (overrideActive) {\n const ov = profileValues(overrideProfile);\n if (ov) {\n targetCurrentA = ov.current_a;\n targetVoltageV = ov.voltage_v;\n effectiveProfile = `override_${overrideProfile}`;\n mode = `${mode}_override`;\n reason.unshift(\n `Override actif jusqu'à ${overrideUntil} | profil=${overrideProfile}` +\n (overrideReason ? ` | ${overrideReason}` : '')\n );\n }\n} else if (overrideEnabled && overrideProfile !== 'auto') {\n reason.push(\"Override configuré mais inactif ou expiré\");\n}\n\ntargetCurrentA = round1(targetCurrentA);\ntargetVoltageV = round1(targetVoltageV);\n\nconst shouldPublishCurrent =\n !Number.isFinite(currentMaxChargeCurrentA) ||\n Math.abs(targetCurrentA - currentMaxChargeCurrentA) >= cfg.currentPublishDelta;\n\nconst shouldPublishVoltage =\n !Number.isFinite(currentMaxChargeVoltageV) ||\n Math.abs(targetVoltageV - currentMaxChargeVoltageV) >= cfg.voltagePublishDelta;\n\nconst shortReason = reason.join(' | ').slice(0, 255);\n\nconst signature = [\n now.date,\n mode,\n effectiveProfile,\n `i=${targetCurrentA}`,\n `v=${targetVoltageV}`\n].join('|');\n\nconst signatureChanged = signature !== lastSignature;\n\nconst result = {\n timestamp_paris: now.localStamp,\n\n inputs: {\n soc_percent: soc,\n battery_voltage_v: Number.isFinite(batteryVoltageV) ? batteryVoltageV : null,\n battery_current_a: Number.isFinite(batteryCurrentA) ? batteryCurrentA : null,\n pv_power_w: pvPowerW,\n edf_switch_observed: edfSwitch,\n today_remaining_kwh: todayRemainingKwh,\n tomorrow_kwh: tomorrowKwh,\n current_max_charge_current_a: currentMaxChargeCurrentA,\n current_max_charge_voltage_v: currentMaxChargeVoltageV,\n auto_enabled: autoEnabled,\n notifications_enabled: notificationsEnabled,\n last_signature: lastSignature || null,\n\n override_enabled: overrideEnabled,\n override_profile: overrideProfile,\n override_current_a: Number.isFinite(overrideCurrentA) ? overrideCurrentA : null,\n override_voltage_v: Number.isFinite(overrideVoltageV) ? overrideVoltageV : null,\n override_until: overrideUntil || null,\n override_reason: overrideReason || null\n },\n\n computed: {\n is_winter: isWinter,\n in_night_hc: inNightHC,\n in_midday_hc: inMiddayHC,\n in_day_window: inDayWindow,\n forecast_reference_kwh: round1(forecastRefKwh),\n forecast_state: forecastState,\n override_active: overrideActive\n },\n\n decision: {\n mode,\n effective_profile: effectiveProfile,\n target_current_a: targetCurrentA,\n target_voltage_v: targetVoltageV,\n should_publish_current: shouldPublishCurrent,\n should_publish_voltage: shouldPublishVoltage,\n critical_alert: criticalAlert,\n signature,\n signature_changed: signatureChanged,\n reason,\n short_reason: shortReason\n },\n\n ha_service_calls: {\n helper_current: {\n domain: 'input_number',\n service: 'set_value',\n data: {\n entity_id: 'input_number.victron_target_charge_current',\n value: targetCurrentA\n }\n },\n\n helper_voltage: {\n domain: 'input_number',\n service: 'set_value',\n data: {\n entity_id: 'input_number.victron_target_charge_voltage',\n value: targetVoltageV\n }\n },\n\n helper_soc_live: {\n domain: 'input_number',\n service: 'set_value',\n data: {\n entity_id: 'input_number.victron_soc_live',\n value: soc\n }\n },\n\n helper_mode: {\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.victron_charge_mode',\n value: mode\n }\n },\n\n helper_reason: {\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.victron_charge_reason',\n value: shortReason || 'Aucune raison'\n }\n },\n\n helper_profile: {\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.victron_effective_charge_profile',\n value: effectiveProfile\n }\n },\n\n helper_signature: {\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.victron_charge_last_signature',\n value: signature\n }\n },\n\n mqtt_current: {\n domain: 'mqtt',\n service: 'publish',\n data: {\n topic: MQTT_TOPIC_CHARGE_CURRENT,\n payload: JSON.stringify({ value: targetCurrentA }),\n retain: false\n }\n },\n\n mqtt_voltage: {\n domain: 'mqtt',\n service: 'publish',\n data: {\n topic: MQTT_TOPIC_CHARGE_VOLTAGE,\n payload: JSON.stringify({ value: targetVoltageV }),\n retain: false\n }\n },\n\n logbook: {\n domain: 'logbook',\n service: 'log',\n data: {\n name: 'Victron Auto Charge',\n message: `${mode} | profile=${effectiveProfile} | I=${targetCurrentA}A | V=${targetVoltageV}V | SOC=${soc}% | ${shortReason}`,\n entity_id: 'sensor.victron_battery_soc'\n }\n },\n\n mobile_notification: {\n domain: cfg.mobileNotifyDomain,\n service: cfg.mobileNotifyService,\n data: {\n title: 'Victron Auto Charge',\n message: `${mode} | profile=${effectiveProfile} | I=${targetCurrentA}A | V=${targetVoltageV}V | SOC=${soc}%`,\n data: {\n push: {\n \"interruption-level\": \"time-sensitive\",\n sound: { name: \"default\" }\n },\n ttl: 0,\n priority: \"high\"\n }\n }\n }\n }\n};\n\nreturn [{ json: result }];\n" + "jsCode": "function num(v, fallback = NaN) {\n const n = Number.parseFloat(v);\n return Number.isFinite(n) ? n : fallback;\n}\n\nfunction low(v) {\n return (v ?? '').toString().trim().toLowerCase();\n}\n\nfunction parseMaybeJson(v) {\n if (typeof v !== 'string') return v;\n try {\n return JSON.parse(v);\n } catch {\n return v;\n }\n}\n\nfunction extractSnapshot(input) {\n if (typeof input === 'string') return parseMaybeJson(input);\n if (input?.data && typeof input.data === 'string') return parseMaybeJson(input.data);\n if (input?.body && typeof input.body === 'string') return parseMaybeJson(input.body);\n if (input?.data && typeof input.data === 'object') return input.data;\n if (input?.body && typeof input.body === 'object') return input.body;\n return input;\n}\n\nfunction round1(v) {\n return Math.round(v * 10) / 10;\n}\n\nfunction timeToMinutes(hh, mm) {\n return hh * 60 + mm;\n}\n\nfunction inRange(minNow, startMin, endMin) {\n return minNow >= startMin && minNow < endMin;\n}\n\nfunction getParisNowParts() {\n const fmt = new Intl.DateTimeFormat('fr-FR', {\n timeZone: 'Europe/Paris',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false,\n });\n\n const parts = Object.fromEntries(\n fmt.formatToParts(new Date())\n .filter(p => p.type !== 'literal')\n .map(p => [p.type, p.value])\n );\n\n const date = `${parts.year}-${parts.month}-${parts.day}`;\n const localStamp = `${date} ${parts.hour}:${parts.minute}:${parts.second}`;\n\n return {\n year: Number(parts.year),\n month: Number(parts.month),\n day: Number(parts.day),\n hour: Number(parts.hour),\n minute: Number(parts.minute),\n second: Number(parts.second),\n date,\n localStamp,\n minNow: Number(parts.hour) * 60 + Number(parts.minute),\n };\n}\n\nfunction forecastBand(kwh) {\n if (!Number.isFinite(kwh)) return 'unknown';\n if (kwh >= 25) return 'sunny';\n if (kwh >= 12) return 'mixed';\n return 'dark';\n}\n\nconst cfg = {\n winterMonths: [11, 12, 1, 2],\n\n windows: {\n nightHC_start: timeToMinutes(1, 24),\n nightHC_end: timeToMinutes(7, 24),\n\n middayHC_start: timeToMinutes(12, 24),\n middayHC_end: timeToMinutes(14, 24),\n\n winterDay_start: timeToMinutes(7, 15),\n winterDay_end: timeToMinutes(16, 45),\n\n summerDay_start: timeToMinutes(7, 30),\n summerDay_end: timeToMinutes(19, 30),\n },\n\n profiles: {\n night_slow: { current_a: 30, voltage_v: 50.1 },\n takeover_hold: { current_a: 2, voltage_v: 50.1 },\n pv_boost: { current_a: 180, voltage_v: 51.5 },\n pv_mid: { current_a: 160, voltage_v: 51.5 },\n pv_topoff: { current_a: 80, voltage_v: 52.4 },\n pv_hold: { current_a: 30, voltage_v: 50.6 },\n midday_grid_catchup: { current_a: 30, voltage_v: 50.8 },\n emergency_rescue: { current_a: 60, voltage_v: 50.4 },\n },\n\n voltagePublishDelta: 0.05,\n currentPublishDelta: 0.5,\n\n mobileNotifyDomain: 'notify',\n mobileNotifyService: 'mobile_app_iphone_16_de_tt'\n};\n\nconst MQTT_TOPIC_CHARGE_CURRENT = 'W/c0619ab34ac5/settings/0/Settings/SystemSetup/MaxChargeCurrent';\nconst MQTT_TOPIC_CHARGE_VOLTAGE = 'W/c0619ab34ac5/settings/0/Settings/SystemSetup/MaxChargeVoltage';\n\nconst raw = $input.first().json;\nconst input = extractSnapshot(raw);\nconst now = getParisNowParts();\n\nconst soc = num(input.soc_percent, 0);\nconst batteryVoltageV = num(input.battery_voltage_v, NaN);\nconst batteryCurrentA = num(input.battery_current_a, NaN);\nconst pvPowerW = num(input.pv_power_w, 0);\nconst edfSwitch = low(input.edf_switch || 'off') === 'on' ? 'on' : 'off';\n\nconst todayRemainingKwh = num(input.today_remaining_kwh, 0);\nconst tomorrowKwh = num(input.tomorrow_kwh, 0);\n\nconst currentMaxChargeCurrentA = num(input.current_max_charge_current_a, 0);\nconst currentMaxChargeVoltageV = num(input.current_max_charge_voltage_v, 0);\n\nconst autoEnabled = low(input.auto_enabled || 'off') === 'on';\nconst nightTakeover = low(input.night_takeover || 'off') === 'on';\nconst nightTargetSoc = num(input.night_target_soc, 35);\nconst notificationsEnabled = low(input.notifications_enabled || 'off') === 'on';\nconst lastSignature = (input.last_signature ?? '').toString().trim();\n\nconst overrideEnabled = low(input.override_enabled || 'off') === 'on';\nconst overrideProfile = low(input.override_profile || 'auto');\nconst overrideCurrentA = num(input.override_current_a, NaN);\nconst overrideVoltageV = num(input.override_voltage_v, NaN);\nconst overrideUntil = (input.override_until ?? '').toString().trim();\nconst overrideReason = (input.override_reason ?? '').toString().trim();\n\nconst isWinter = cfg.winterMonths.includes(now.month);\nconst inNightHC = inRange(now.minNow, cfg.windows.nightHC_start, cfg.windows.nightHC_end);\nconst inMiddayHC = inRange(now.minNow, cfg.windows.middayHC_start, cfg.windows.middayHC_end);\nconst inDayWindow = isWinter\n ? inRange(now.minNow, cfg.windows.winterDay_start, cfg.windows.winterDay_end)\n : inRange(now.minNow, cfg.windows.summerDay_start, cfg.windows.summerDay_end);\n\nconst forecastRefKwh = now.hour < 8 ? Math.max(todayRemainingKwh, tomorrowKwh, 0) : todayRemainingKwh;\nconst forecastState = forecastBand(forecastRefKwh);\n\nfunction profileValues(profileName) {\n if (profileName === 'manual') {\n return {\n current_a: Number.isFinite(overrideCurrentA) ? overrideCurrentA : cfg.profiles.pv_boost.current_a,\n voltage_v: Number.isFinite(overrideVoltageV) ? overrideVoltageV : cfg.profiles.pv_boost.voltage_v,\n };\n }\n return cfg.profiles[profileName] || null;\n}\n\nconst overrideActive =\n overrideEnabled &&\n overrideProfile !== 'auto' &&\n /^\\d{4}-\\d{2}-\\d{2}/.test(overrideUntil) &&\n now.localStamp < overrideUntil;\n\nlet mode = 'idle';\nlet effectiveProfile = 'auto_idle';\nlet targetCurrentA = currentMaxChargeCurrentA || 30;\nlet targetVoltageV = currentMaxChargeVoltageV || 50.5;\nlet criticalAlert = false;\nlet reason = [];\n\nif (!autoEnabled) {\n mode = 'automation_disabled';\n effectiveProfile = 'manual_keep_current';\n targetCurrentA = currentMaxChargeCurrentA || 30;\n targetVoltageV = currentMaxChargeVoltageV || 50.5;\n reason.push(\"Automatisation désactivée, conservation des réglages actuels\");\n} else {\n if (soc < 20 && !inNightHC && !inMiddayHC) {\n mode = 'emergency_rescue';\n effectiveProfile = 'emergency_rescue';\n targetCurrentA = cfg.profiles.emergency_rescue.current_a;\n targetVoltageV = cfg.profiles.emergency_rescue.voltage_v;\n criticalAlert = true;\n reason.push(`SOC critique ${soc}%`);\n }\n\n // P4 : takeover EVCS nocturne. EDF est ON pour la maison + le VE (HC),\n // la batterie doit seulement être RAMENÉE à la cible matin puis MAINTENUE,\n // pas rechargée à fond (cible adaptée au forecast : été ~35%, hiver ~85%).\n else if (nightTakeover && edfSwitch === 'on') {\n const holdTargetSoc = Math.min(95, Math.max(20, nightTargetSoc));\n if (soc < holdTargetSoc - 1) {\n mode = 'evcs_takeover_recover';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(`Takeover EVCS : recharge vers cible matin ${holdTargetSoc}% (SOC ${soc}%)`);\n } else {\n mode = 'evcs_takeover_hold';\n effectiveProfile = 'takeover_hold';\n targetCurrentA = cfg.profiles.takeover_hold.current_a;\n targetVoltageV = cfg.profiles.takeover_hold.voltage_v;\n reason.push(`Takeover EVCS : maintien SOC ~${holdTargetSoc}% (maison+VE sur HC, batterie en pause)`);\n }\n }\n\n else if (isWinter && inNightHC) {\n let targetSoc;\n if (forecastState === 'sunny') targetSoc = 40;\n else if (forecastState === 'mixed') targetSoc = 60;\n else targetSoc = 80;\n\n if (soc < targetSoc - 1) {\n mode = 'winter_night_charge_window';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(`Hiver HC nuit, cible SOC ${targetSoc}% selon forecast ${forecastState}`);\n } else {\n mode = 'winter_night_idle';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(`Hiver HC nuit, SOC ${soc}% déjà suffisant`);\n }\n }\n\n else if (!isWinter && inNightHC) {\n if (soc < 22) {\n mode = 'summer_night_rescue_window';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(\"Été HC nuit, secours SOC < 22%\");\n } else {\n mode = 'summer_night_idle';\n effectiveProfile = 'night_slow';\n targetCurrentA = cfg.profiles.night_slow.current_a;\n targetVoltageV = cfg.profiles.night_slow.voltage_v;\n reason.push(\"Été HC nuit, pas de besoin réseau\");\n }\n }\n\n else if (\n inMiddayHC &&\n soc < 85 &&\n forecastRefKwh < 8 &&\n pvPowerW < 1500\n ) {\n mode = 'midday_catchup_window';\n effectiveProfile = 'midday_grid_catchup';\n targetCurrentA = cfg.profiles.midday_grid_catchup.current_a;\n targetVoltageV = cfg.profiles.midday_grid_catchup.voltage_v;\n reason.push(`HC midi rattrapage : SOC ${soc}% | forecast ${forecastRefKwh} kWh | PV ${pvPowerW} W`);\n }\n\n else if (inDayWindow) {\n if (soc < 80) {\n mode = 'day_pv_boost';\n effectiveProfile = 'pv_boost';\n targetCurrentA = cfg.profiles.pv_boost.current_a;\n targetVoltageV = cfg.profiles.pv_boost.voltage_v;\n reason.push(`Jour SOC bas ${soc}% : capture PV max`);\n } else if (soc < 90) {\n mode = 'day_pv_mid';\n effectiveProfile = 'pv_mid';\n targetCurrentA = cfg.profiles.pv_mid.current_a;\n targetVoltageV = cfg.profiles.pv_mid.voltage_v;\n reason.push(`Jour SOC intermédiaire ${soc}%`);\n } else if (soc < 95) {\n mode = 'day_pv_topoff';\n effectiveProfile = 'pv_topoff';\n targetCurrentA = cfg.profiles.pv_topoff.current_a;\n targetVoltageV = cfg.profiles.pv_topoff.voltage_v;\n reason.push(`Jour top-off vers 95% | SOC ${soc}%`);\n } else {\n mode = 'day_pv_hold';\n effectiveProfile = 'pv_hold';\n targetCurrentA = cfg.profiles.pv_hold.current_a;\n targetVoltageV = cfg.profiles.pv_hold.voltage_v;\n reason.push(`Jour maintien haut SOC ${soc}%`);\n }\n\n if (\n Number.isFinite(batteryVoltageV) &&\n soc < 95 &&\n pvPowerW > 2000 &&\n batteryVoltageV >= (targetVoltageV - 0.15)\n ) {\n targetVoltageV = Math.max(targetVoltageV, 51.3);\n targetCurrentA = Math.max(targetCurrentA, 80);\n if (soc >= 90) {\n effectiveProfile = 'pv_topoff';\n mode = 'day_pv_topoff';\n } else {\n effectiveProfile = 'pv_boost';\n }\n reason.push(`Batterie proche consigne (${round1(batteryVoltageV)}V), relèvement anti-bridage`);\n }\n\n if (\n Number.isFinite(batteryCurrentA) &&\n soc < 95 &&\n pvPowerW > 6000 &&\n batteryCurrentA >= (targetCurrentA - 3)\n ) {\n targetCurrentA = Math.max(targetCurrentA,\n // Valeurs de la table, et non des constantes qui prennent du retard.\n // ⚠️ Ce Math.max est un NO-OP : le plafond du profil est au-dessus du\n // plancher dans toutes les bandes. Constate le 14/09/2026 ; le\n // mecanisme n'a pas ete ranime deliberement (un plafond qui se releve\n // parce qu'on bute dessus est une boucle de retroaction).\n soc >= 90 ? cfg.profiles.pv_topoff.current_a : cfg.profiles.pv_mid.current_a);\n reason.push(`Courant batterie proche limite (${round1(batteryCurrentA)}A), relèvement du plafond`);\n }\n }\n\n\n else {\n mode = 'off_window_idle';\n effectiveProfile = 'pv_hold';\n targetCurrentA = cfg.profiles.pv_hold.current_a;\n targetVoltageV = cfg.profiles.pv_hold.voltage_v;\n reason.push(\"Hors fenêtre utile\");\n }\n}\n\nif (overrideActive) {\n const ov = profileValues(overrideProfile);\n if (ov) {\n targetCurrentA = ov.current_a;\n targetVoltageV = ov.voltage_v;\n effectiveProfile = `override_${overrideProfile}`;\n mode = `${mode}_override`;\n reason.unshift(\n `Override actif jusqu'à ${overrideUntil} | profil=${overrideProfile}` +\n (overrideReason ? ` | ${overrideReason}` : '')\n );\n }\n} else if (overrideEnabled && overrideProfile !== 'auto') {\n reason.push(\"Override configuré mais inactif ou expiré\");\n}\n\ntargetCurrentA = round1(targetCurrentA);\ntargetVoltageV = round1(targetVoltageV);\n\nconst shouldPublishCurrent =\n !Number.isFinite(currentMaxChargeCurrentA) ||\n Math.abs(targetCurrentA - currentMaxChargeCurrentA) >= cfg.currentPublishDelta;\n\nconst shouldPublishVoltage =\n !Number.isFinite(currentMaxChargeVoltageV) ||\n Math.abs(targetVoltageV - currentMaxChargeVoltageV) >= cfg.voltagePublishDelta;\n\nconst shortReason = reason.join(' | ').slice(0, 255);\n\nconst signature = [\n now.date,\n mode,\n effectiveProfile,\n `i=${targetCurrentA}`,\n `v=${targetVoltageV}`\n].join('|');\n\nconst signatureChanged = signature !== lastSignature;\n\nconst result = {\n timestamp_paris: now.localStamp,\n\n inputs: {\n soc_percent: soc,\n battery_voltage_v: Number.isFinite(batteryVoltageV) ? batteryVoltageV : null,\n battery_current_a: Number.isFinite(batteryCurrentA) ? batteryCurrentA : null,\n pv_power_w: pvPowerW,\n edf_switch_observed: edfSwitch,\n today_remaining_kwh: todayRemainingKwh,\n tomorrow_kwh: tomorrowKwh,\n current_max_charge_current_a: currentMaxChargeCurrentA,\n current_max_charge_voltage_v: currentMaxChargeVoltageV,\n auto_enabled: autoEnabled,\n notifications_enabled: notificationsEnabled,\n last_signature: lastSignature || null,\n\n override_enabled: overrideEnabled,\n override_profile: overrideProfile,\n override_current_a: Number.isFinite(overrideCurrentA) ? overrideCurrentA : null,\n override_voltage_v: Number.isFinite(overrideVoltageV) ? overrideVoltageV : null,\n override_until: overrideUntil || null,\n override_reason: overrideReason || null\n },\n\n computed: {\n is_winter: isWinter,\n in_night_hc: inNightHC,\n in_midday_hc: inMiddayHC,\n in_day_window: inDayWindow,\n forecast_reference_kwh: round1(forecastRefKwh),\n forecast_state: forecastState,\n override_active: overrideActive\n },\n\n decision: {\n mode,\n effective_profile: effectiveProfile,\n target_current_a: targetCurrentA,\n target_voltage_v: targetVoltageV,\n should_publish_current: shouldPublishCurrent,\n should_publish_voltage: shouldPublishVoltage,\n critical_alert: criticalAlert,\n signature,\n signature_changed: signatureChanged,\n reason,\n short_reason: shortReason\n },\n\n ha_service_calls: {\n helper_current: {\n domain: 'input_number',\n service: 'set_value',\n data: {\n entity_id: 'input_number.victron_target_charge_current',\n value: targetCurrentA\n }\n },\n\n helper_voltage: {\n domain: 'input_number',\n service: 'set_value',\n data: {\n entity_id: 'input_number.victron_target_charge_voltage',\n value: targetVoltageV\n }\n },\n\n helper_soc_live: {\n domain: 'input_number',\n service: 'set_value',\n data: {\n entity_id: 'input_number.victron_soc_live',\n value: soc\n }\n },\n\n helper_mode: {\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.victron_charge_mode',\n value: mode\n }\n },\n\n helper_reason: {\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.victron_charge_reason',\n value: shortReason || 'Aucune raison'\n }\n },\n\n helper_profile: {\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.victron_effective_charge_profile',\n value: effectiveProfile\n }\n },\n\n helper_signature: {\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.victron_charge_last_signature',\n value: signature\n }\n },\n\n mqtt_current: {\n domain: 'mqtt',\n service: 'publish',\n data: {\n topic: MQTT_TOPIC_CHARGE_CURRENT,\n payload: JSON.stringify({ value: targetCurrentA }),\n retain: false\n }\n },\n\n mqtt_voltage: {\n domain: 'mqtt',\n service: 'publish',\n data: {\n topic: MQTT_TOPIC_CHARGE_VOLTAGE,\n payload: JSON.stringify({ value: targetVoltageV }),\n retain: false\n }\n },\n\n logbook: {\n domain: 'logbook',\n service: 'log',\n data: {\n name: 'Victron Auto Charge',\n message: `${mode} | profile=${effectiveProfile} | I=${targetCurrentA}A | V=${targetVoltageV}V | SOC=${soc}% | ${shortReason}`,\n entity_id: 'sensor.victron_battery_soc'\n }\n },\n\n mobile_notification: {\n domain: cfg.mobileNotifyDomain,\n service: cfg.mobileNotifyService,\n data: {\n title: 'Victron Auto Charge',\n message: `${mode} | profile=${effectiveProfile} | I=${targetCurrentA}A | V=${targetVoltageV}V | SOC=${soc}%`,\n data: {\n push: {\n \"interruption-level\": \"time-sensitive\",\n sound: { name: \"default\" }\n },\n ttl: 0,\n priority: \"high\"\n }\n }\n }\n }\n};\n\nreturn [{ json: result }];\n" }, "id": "a98c5bda-cf34-447e-942e-37ae75a45d5c", "name": "Compute Charge Targets",