Compare commits
15 Commits
cf7eb47356
...
a7ebaabb8b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7ebaabb8b | ||
|
|
32de4ffa35 | ||
|
|
c3eeab2c92 | ||
|
|
57c696928d | ||
|
|
ccd764beb9 | ||
|
|
807ae45b7f | ||
|
|
a57ff19ead | ||
|
|
92f07fe885 | ||
|
|
fc49e6c31a | ||
|
|
f0479945c4 | ||
|
|
f07d4cb0f1 | ||
|
|
40f34418d6 | ||
|
|
aa83b5c3d9 | ||
|
|
7a1775489f | ||
|
|
457f4c8727 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,3 +8,9 @@ node_modules/
|
|||||||
|
|
||||||
# Jeton HA local (ne jamais versionner)
|
# Jeton HA local (ne jamais versionner)
|
||||||
.env
|
.env
|
||||||
|
deploy/watchdog/watchdog.env
|
||||||
|
deploy/watchdog/logs/*
|
||||||
|
!deploy/watchdog/logs/.gitkeep
|
||||||
|
|
||||||
|
# Journal du chien de garde (churn permanent, aucune valeur en historique)
|
||||||
|
logs/
|
||||||
|
|||||||
751
HANDOFF.md
751
HANDOFF.md
@@ -1601,3 +1601,754 @@ du bilan énergétique partagé plutôt qu'un garde de plus.
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Document mis à jour le : 24/08/2026 (~12:50) — Fix G retiré (golden-hysteresis 7/7, seuils réels 20/30), inversion démarrage/maintien corrigée (golden 40/40), diagnostic des deux oscillateurs*
|
*Document mis à jour le : 24/08/2026 (~12:50) — Fix G retiré (golden-hysteresis 7/7, seuils réels 20/30), inversion démarrage/maintien corrigée (golden 40/40), diagnostic des deux oscillateurs*
|
||||||
|
|
||||||
|
## 27. Session du 30/08/2026 — Deux propriétaires GX de la décision réseau, jamais lus
|
||||||
|
|
||||||
|
Le propriétaire demandait une procédure détaillée avant d'intervenir sur les
|
||||||
|
deux MultiPlus-II en parallèle (piste VEConfigure de la §26). Les lectures
|
||||||
|
préalables côté GX déplacent la priorité : **il est probable qu'aucune
|
||||||
|
intervention VEConfigure ne soit nécessaire.**
|
||||||
|
|
||||||
|
Procédure complète : `docs/PROCEDURE_ESS_PARALLELE.md`.
|
||||||
|
|
||||||
|
### Trouvaille 1 — Consigne de puissance réseau à 4200 W (défaut : 50 W)
|
||||||
|
`number.gx_device_ac_power_setpoint` = **4200 W**, miroir per-phase
|
||||||
|
`number.airtria_mpii_v1_ac_power_setpoint_l1` = 4200 W. C'est le *Grid setpoint*
|
||||||
|
ESS (`/Settings/CGwacs/AcPowerSetPoint`) : la puissance que le système
|
||||||
|
s'efforce en permanence de **prélever sur le réseau** quand AC-IN est présent.
|
||||||
|
|
||||||
|
L'ESS régulant la puissance au point de raccordement, le seul levier pour faire
|
||||||
|
place à cet import quand maison et batterie sont déjà servies est de **brider
|
||||||
|
les MPPT via DVCC**. C'est le mécanisme exact de la substitution prouvée le
|
||||||
|
27/08, et l'explication du résidu constant (~1040 W).
|
||||||
|
|
||||||
|
Les deux campagnes se réconcilient arithmétiquement :
|
||||||
|
- 10:47, SOC 71 %, conso 877 W → import monte à **4113 W** ≈ consigne ;
|
||||||
|
- 11:38, SOC 86 %, batterie en absorption n'acceptant que ~1070 W →
|
||||||
|
`conso 2258 + batterie 1070 − PV 1043 = 2285 W` contre **2287 W** mesurés.
|
||||||
|
|
||||||
|
Valeur inchangée sur tout l'historique disponible (7 j) ; `user_id: null` sur le
|
||||||
|
dernier changement = simple recréation d'entité au redémarrage HA du 24/08.
|
||||||
|
Aucun code du dépôt n'écrit cette entité (grep). Hypothèse la plus probable :
|
||||||
|
ancien contournement manuel, même famille que les charges programmées ESS
|
||||||
|
désactivées le 27/08. **À confirmer sur l'écran du GX avant de conclure** — ne
|
||||||
|
pas se fier au seul nom de l'entité HA.
|
||||||
|
|
||||||
|
Correction proposée : **50 W** (défaut Victron, légèrement positif pour ne
|
||||||
|
jamais injecter). Réversible en dix secondes, sans VEConfigure.
|
||||||
|
|
||||||
|
### Trouvaille 2 — SOC minimal ESS à 40 %, au-dessus de notre cible de nuit
|
||||||
|
`number.gx_device_ess_min_soc_limit` = 40 %, `sensor.gx_device_active_soc_limit`
|
||||||
|
= 40 % (BatteryLife ne fait donc pas dériver la limite au-dessus).
|
||||||
|
|
||||||
|
Sous 40 %, l'ESS recharge la batterie sur le réseau **de sa propre initiative**.
|
||||||
|
Ce n'est pas théorique : `sensor.gx_device_ess_batterylife_state` passe à
|
||||||
|
`recharge` les 29/08 04:08 et 30/08 01:21, précédé de
|
||||||
|
`soc_below_battery_life_dynamic_soc_limit`.
|
||||||
|
|
||||||
|
Or le contrôleur vise **35 %** le matin en été : le nôtre relâche à 35, le GX
|
||||||
|
reprend à 40. **Troisième automate contradictoire sur la même batterie**, après
|
||||||
|
les charges programmées. Arbitrage à rendre avec le propriétaire — piste
|
||||||
|
préférée : laisser ce plancher comme filet de sécurité bas (~20 %) et confier la
|
||||||
|
vraie cible au bilan énergétique. Rien changé sans son accord.
|
||||||
|
|
||||||
|
### Trouvaille 3 — L'assistant ESS tourne, aucune erreur VE.Bus
|
||||||
|
États BatteryLife vivants et cohérents → la boucle ESS fonctionne, elle n'est
|
||||||
|
pas absente. Toutes les alarmes VE.Bus à `no_alarm`, aucune erreur
|
||||||
|
d'installation. L'hypothèse « assistant perdu ou dépareillé entre les deux
|
||||||
|
unités après le passage en parallèle » est **affaiblie** sans être éliminée (un
|
||||||
|
assistant présent mais mal paramétré ne lève aucune alarme) — d'où le maintien
|
||||||
|
de la partie VEConfigure en seconde intention dans la procédure.
|
||||||
|
|
||||||
|
### Méthode retenue dans la procédure
|
||||||
|
Étapes classées par risque croissant : lectures gratuites → réglages GX
|
||||||
|
réversibles → **rejeu du protocole alterné du 27/08** comme critère objectif →
|
||||||
|
VEConfigure seulement en cas d'échec, avec sauvegarde `.rvms` obligatoire, une
|
||||||
|
correction à la fois, et interdiction absolue de relancer le VE.Bus System
|
||||||
|
Configurator. Firmware : toujours pas justifié par les mesures, et à ne pas
|
||||||
|
mêler au diagnostic.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis à jour le : 30/08/2026 — consigne réseau 4200 W et SOC minimal ESS 40 % identifiés comme propriétaires non lus de la décision réseau ; procédure d'intervention rédigée*
|
||||||
|
|
||||||
|
## 28. Session du 31/08/2026 — La substitution ne se reproduit plus (batterie preneuse). Cas « batterie pleine » encore ouvert
|
||||||
|
|
||||||
|
Première vérification de la correction de la consigne réseau (4200 → 50 W,
|
||||||
|
§27), sur une nuit complète puis deux campagnes alternées.
|
||||||
|
|
||||||
|
### La nuit 30-31/08 : l'import colle à la consommation
|
||||||
|
Une seule session réseau, `switch.edf_general_ok_switch` ON à **05:04:10**
|
||||||
|
(hystérésis, SOC 21 ≤ seuil 20) et OFF à **10:08:04** (SOC 30 ≥ seuil OFF).
|
||||||
|
Aucun rebond, aucun créneau.
|
||||||
|
|
||||||
|
| heure | import réseau | conso maison | courant batterie |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| 05:15 | 780 W | 732 W | 0 A |
|
||||||
|
| 06:30 | 507 W | 477 W | −1 A |
|
||||||
|
| 07:45 | 781 W | 751 W | +4 A |
|
||||||
|
|
||||||
|
À comparer au test du 27/08 10:47 où, pour 877 W de consommation, l'import
|
||||||
|
montait en rampe jusqu'à **4113 W** pour gaver la batterie. Le comportement a
|
||||||
|
disparu. Détail de la fermeture : de 05:04 à 05:13 l'import est resté à
|
||||||
|
**24-56 W** (la consigne) pendant que la batterie continuait de servir la maison
|
||||||
|
à −15 A ; à 05:14, SOC atteignant le plancher ESS de 20 %, l'ESS a basculé —
|
||||||
|
import = charge maison, batterie à 0 A, SOC plat à 20 % jusqu'à 08:00. Manuel.
|
||||||
|
|
||||||
|
### Le matin : réseau présent, production libre
|
||||||
|
De 08:40 à la coupure de 10:08, contacteur toujours fermé :
|
||||||
|
|
||||||
|
| heure | PV | import | conso | I batterie |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| 08:45 | 1 303 W | **18 W** | 1 109 W | +2 A |
|
||||||
|
| 09:25 | 3 425 W | **39 W** | 805 W | +50 A |
|
||||||
|
| 10:05 | **5 244 W** | **47 W** | 952 W | +83 A |
|
||||||
|
|
||||||
|
`sensor.mppt_450_200_hq2449yr9pt_mppt_operation_mode` est resté `mppt_active`
|
||||||
|
sur toute la fenêtre : le chargeur déclare lui-même chercher son point de
|
||||||
|
puissance maximale pendant que le réseau est présent.
|
||||||
|
|
||||||
|
### Campagnes alternées (`tools/test-edf-alterne.mjs`)
|
||||||
|
Journaux : `docs/mesures/20260831-1043-test-edf-alterne.log` (phases 180 s) et
|
||||||
|
`20260831-1101-test-edf-alterne.log` (phases 240 s).
|
||||||
|
|
||||||
|
Transitions exploitables, chaque phase fermée contre ses voisines ouvertes :
|
||||||
|
|
||||||
|
| phase | PV fermée | PV voisines ouvertes | écart | trackers bridés |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| 10:47 ON_1 | 7 168 W | 7 131 W | **+0,5 %** | 2,7/7 (DVCC, I=124 A) |
|
||||||
|
| 11:13 ON_2 | 7 920 W (max 8 759) | 7 002 W | **+13,1 %** | 4,5/7 (DVCC, I=125 A) |
|
||||||
|
|
||||||
|
Le 27/08, la même manœuvre faisait tomber la PV de 3 885 à 1 045 W en 90 s avec
|
||||||
|
six trackers sur sept en `voltage_current_limited`. **La substitution ne se
|
||||||
|
reproduit plus à batterie preneuse.** Le bridage résiduel observé est le bridage
|
||||||
|
NORMAL : il apparaît quand le courant batterie approche la limite DVCC (125 sur
|
||||||
|
130 A) et il s'accompagne d'un import réseau nul — critère de discrimination
|
||||||
|
utilisable : bridage normal ⇒ import nul ; bridage anormal ⇒ l'import remplace
|
||||||
|
la production.
|
||||||
|
|
||||||
|
### Mécanisme identifié : un transitoire de reprise, qui retombe à la consigne
|
||||||
|
À chaque fermeture du contacteur, le réseau reprend d'un coup **toute** la charge
|
||||||
|
de la maison et la totalité du solaire bascule vers la batterie. Bilan vérifié au
|
||||||
|
watt près (10:48:15 : PV 7222 + grid 2120 − conso 2249 = 7093 W contre 7107 W de
|
||||||
|
charge batterie mesurée). Puis l'import **décroît jusqu'à la consigne** :
|
||||||
|
|
||||||
|
| campagne | import à la fermeture | après ~2 min 15 |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 11:05 ON_1 | 1 985 W | **25 W**, stable |
|
||||||
|
| 11:13 ON_2 | 2 600 W | **52 W** puis −41 W |
|
||||||
|
|
||||||
|
Coût ≈ 50-60 Wh par fermeture. Avec l'ancienne consigne à 4200 W, cette reprise
|
||||||
|
n'avait aucune raison de redescendre : elle se stabilisait sur « le réseau
|
||||||
|
alimente tout, on bride le solaire pour faire de la place ». C'est
|
||||||
|
vraisemblablement le même mécanisme, dont la correction de consigne a rétabli
|
||||||
|
l'extinction. Corollaire : chaque fermeture coûte, ce qui plaide pour une
|
||||||
|
session réseau unique et dimensionnée plutôt que des créneaux répétés.
|
||||||
|
Note : aucun transitoire à la fermeture de 05:04 (PV nulle) — il n'y a rien à
|
||||||
|
rediriger quand il n'y a pas de solaire.
|
||||||
|
|
||||||
|
### CE QUI N'EST TOUJOURS PAS TESTÉ — batterie pleine (à faire)
|
||||||
|
Tout ce qui précède s'est mesuré à SOC 40-54 %, batterie absorbant 60 à 125 A.
|
||||||
|
Le propriétaire maintient, sur plusieurs mois d'observation, que le cas décisif
|
||||||
|
est **batterie pleine** : plus de tampon, donc le solaire qui alimentait la
|
||||||
|
maison n'a plus de destination quand le réseau la reprend. Discrimination
|
||||||
|
attendue, contacteur fermé, maison ~2 kW, SOC ≥ 90 % :
|
||||||
|
|
||||||
|
| observation | conclusion |
|
||||||
|
|---|---|
|
||||||
|
| PV ≈ conso (~2 kW), import ≈ 50 W | corrigé — écrêtage normal, le solaire sert la maison |
|
||||||
|
| PV ≈ 0-1 kW, import ≈ conso | non corrigé — éviction ; VEConfigure (§27, `docs/PROCEDURE_ESS_PARALLELE.md` étape 1) |
|
||||||
|
|
||||||
|
Le 27/08 à 11:38, SOC 86-88 %, batterie n'acceptant plus que ~1 kW, on avait
|
||||||
|
PV 1043 W pour une maison à 2258 W et un import de 2287 W : le solaire SOUS la
|
||||||
|
consommation et le réseau AU-DESSUS. C'est le point le plus proche du cas
|
||||||
|
décisif dont on dispose, et il est du mauvais côté. Trois minutes de protocole
|
||||||
|
alterné à SOC ≥ 90 suffiront à trancher.
|
||||||
|
|
||||||
|
### Outillage ajouté
|
||||||
|
- `tools/test-edf-alterne.mjs` — protocole alterné OFF/ON/OFF/ON/OFF, phases et
|
||||||
|
pas d'échantillonnage paramétrables. Mesure PV, tension et courant batterie,
|
||||||
|
conso, import, SOC, limite DVCC, présence physique AC-IN et le mode des 7
|
||||||
|
chargeurs solaires. **Désactive l'hystérésis pendant le test et restaure tout
|
||||||
|
à la fin, sur Ctrl-C et sur erreur.** Journal horodaté dans `docs/mesures/`.
|
||||||
|
- `tools/analyse-test-alterne.mjs` — résumé par phase et verdict.
|
||||||
|
|
||||||
|
Deux pièges d'instrumentation corrigés en cours de route, à ne pas réintroduire :
|
||||||
|
1. **Garde météo.** Les phases OUVERTES voient toutes la même ressource : si
|
||||||
|
elles divergent entre elles (33 % et 42 % sur les deux campagnes du 31/08),
|
||||||
|
la moyenne ON contre OFF est ininterprétable. L'outil refuse désormais de
|
||||||
|
conclure dans ce cas et compare chaque phase fermée à ses voisines
|
||||||
|
immédiates. C'est ce piège qui avait pollué le premier test du 27/08.
|
||||||
|
Contrôle interne utile : si la PV chute AVANT l'apparition de l'import, ce
|
||||||
|
sont des nuages (cas de la phase ON_2 du 31/08 10:53, chute 6995 → 3607 W
|
||||||
|
avec l'import encore à zéro).
|
||||||
|
2. **Biais du parseur.** La lecture des modes MPPT était ancrée sur `MPPT `, ce
|
||||||
|
qui faisait tomber en silence toute ligne dont le PREMIER tracker était bridé
|
||||||
|
— précisément les échantillons à mesurer — et biaisait le résultat vers
|
||||||
|
« aucun bridage ». Le bloc de modes est le dernier champ : l'ancrer sur
|
||||||
|
`edf on|off`, jamais sur le mot `MPPT`.
|
||||||
|
|
||||||
|
### Signal indépendant : la jauge de SOC s'effondre en bas d'échelle
|
||||||
|
Entre 04:00 et 04:20, le SOC est passé de **30 à 22 %** alors que la batterie ne
|
||||||
|
débitait que ~11 A (≈ 550 W), tension stable à 49 V. Huit points sur un parc de
|
||||||
|
~15 kWh représentent 1,2 kWh, soit plus de deux heures à ce débit — pas vingt
|
||||||
|
minutes. La jauge ne mesure plus, elle réestime. Comportement classique d'un
|
||||||
|
parc vieillissant près du genou de décharge, cohérent avec le module en panne
|
||||||
|
connu (7 modules actifs sur 8, cf. MANUEL §0).
|
||||||
|
Conséquence : tous les seuils des deux workflows (20, 30, 22, 35) sont exprimés
|
||||||
|
dans une grandeur non linéaire en bas d'échelle. À verser au chantier du bilan
|
||||||
|
énergétique ; ne pas « corriger » par un seuil de plus.
|
||||||
|
|
||||||
|
### LE CAS DÉCISIF, TESTÉ — batterie pleine (campagne 12:58, SOC 90-92 %)
|
||||||
|
Journal : `docs/mesures/20260831-1319-test-edf-alterne.log`. Conditions de
|
||||||
|
départ exactement celles que le propriétaire désignait : SOC 90 %, courant
|
||||||
|
batterie **+22,8 A**, DVCC retombée à 80 A, production déjà écrêtée à
|
||||||
|
3 691 W (2 301 W de maison + ~1,1 kW de batterie) alors que la même
|
||||||
|
installation produisait 9 112 W le matin. Plus de tampon.
|
||||||
|
|
||||||
|
**Le phénomène qu'il décrit se produit — et il se résorbe.** ON_2, le cas le
|
||||||
|
plus net :
|
||||||
|
|
||||||
|
| heure | PV | import | conso |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| 13:11:57 | 2 663 W | 0 | 1 899 W |
|
||||||
|
| **13:12:12** | **559 W** | **1 777 W** | 1 886 W |
|
||||||
|
| 13:12:57 | 1 405 W | 1 136 W | 1 877 W |
|
||||||
|
| 13:13:27 | 1 912 W | 683 W | 1 891 W |
|
||||||
|
| 13:14:27 | 3 008 W | 258 W | 2 362 W |
|
||||||
|
| 13:14:42 | **3 158 W** | **112 W** | 2 373 W |
|
||||||
|
|
||||||
|
Effondrement de **79 %** en un seul pas de mesure, simultané à l'apparition de
|
||||||
|
l'import (et non avant, contrairement aux passages nuageux du matin : le
|
||||||
|
critère de discrimination fonctionne). Puis récupération en miroir, watt pour
|
||||||
|
watt, sur ~2 min 30 : l'import redescend à 112 W et la production revient à
|
||||||
|
3 158 W, **au-dessus** de son niveau d'avant fermeture. Même séquence en ON_1
|
||||||
|
(3 369 → 1 890 W avec import 2 083 W, retour à 3 315 W avec import 8 W).
|
||||||
|
|
||||||
|
Une fois le transitoire exclu (165 s), l'état établi ne montre aucune
|
||||||
|
substitution : **−7,0 %** et **+2,0 %** contre les phases ouvertes voisines,
|
||||||
|
import résiduel 443 et 479 W et toujours décroissant. Les phases ouvertes
|
||||||
|
déclinent d'elles-mêmes (3 505 → 2 808 → 2 488 W, milieu d'après-midi), ce qui
|
||||||
|
explique le −7 %.
|
||||||
|
|
||||||
|
**Conclusion.** L'observation de plusieurs mois du propriétaire est exacte : à
|
||||||
|
batterie pleine, fermer le contacteur tue la production. Ce qui était faux,
|
||||||
|
c'est la permanence. Le mécanisme est le même transitoire de reprise qu'à
|
||||||
|
batterie preneuse — le réseau reprend la maison, le solaire cherche une
|
||||||
|
destination — sauf que sans tampon la seule variable d'ajustement est le
|
||||||
|
bridage des MPPT, donc l'effet devient spectaculaire au lieu d'être invisible.
|
||||||
|
Avec l'ancienne consigne à 4200 W, ce transitoire n'avait aucune raison de
|
||||||
|
se terminer : le régulateur ne pouvait atteindre sa cible qu'en maintenant le
|
||||||
|
solaire bridé. **C'est l'explication complète de « pas de grid sinon le
|
||||||
|
solaire ne sert à rien », et la correction de consigne y met fin.**
|
||||||
|
|
||||||
|
Coût par fermeture du contacteur, à batterie pleine : ~50 Wh d'import
|
||||||
|
transitoire **plus** ~100 Wh de production perdue pendant les 2 min 30 de
|
||||||
|
bridage, soit environ **150 Wh**. À batterie preneuse, seulement les ~50 Wh.
|
||||||
|
C'est l'argument chiffré en faveur d'une **session réseau unique et
|
||||||
|
dimensionnée** plutôt que de créneaux répétés : chaque ouverture-fermeture se
|
||||||
|
paie, et se paie trois fois plus cher quand la batterie est pleine.
|
||||||
|
|
||||||
|
Ce qui reste vrai et inchangé : à batterie pleine et régime établi, la
|
||||||
|
production est écrêtée à la consommation (6 à 7 trackers sur 7 en
|
||||||
|
`voltage_current_limited` dans TOUTES les phases, ouvertes comme fermées).
|
||||||
|
C'est le bridage normal d'une installation sans injection, il ne dépend pas du
|
||||||
|
réseau, et il ne doit pas être confondu avec l'éviction.
|
||||||
|
|
||||||
|
**Aucune intervention VEConfigure ni firmware n'est justifiée par ces mesures.**
|
||||||
|
L'étape 1 de `docs/PROCEDURE_ESS_PARALLELE.md` reste en réserve, sans objet
|
||||||
|
actuel.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis à jour le : 31/08/2026 (~13:30) — cas batterie pleine testé : l'effondrement existe mais dure 2 min 30 et se résorbe entierement ; la substitution permanente est morte avec la consigne a 4200 W ; ni VEConfigure ni firmware justifies*
|
||||||
|
|
||||||
|
## 29. Session du 31/08/2026 (suite) — Fenêtre d'établissement du réseau
|
||||||
|
|
||||||
|
### Le défaut
|
||||||
|
Mesure du 31/08 (§28) : à la fermeture du contacteur, le réseau reprend d'un
|
||||||
|
coup toute la charge de la maison et le solaire bascule vers la batterie — ou se
|
||||||
|
fait brider s'il n'y a plus de tampon (2 663 → 559 W). L'import redescend
|
||||||
|
ensuite à la consigne en **~150 s**, deux fois mesuré (1 985 → 25 W, 2 600 → 52 W).
|
||||||
|
|
||||||
|
Pendant cette phase, PV, import et courant batterie ne décrivent **aucun régime
|
||||||
|
permanent**. Or la boucle EVCS tourne toutes les 30 s : elle y prend cinq
|
||||||
|
décisions. Pire, `INPUT_PARSE` lit `pv_power_10m_avg_w`, une moyenne glissante
|
||||||
|
de dix minutes : un creux de 2,5 kW pendant 2 min 30 la tire d'environ 600 W et
|
||||||
|
l'effet persiste une dizaine de minutes après le retour à la normale. Les seuils
|
||||||
|
de démarrage et de maintien solaire se jouent dans cette plage.
|
||||||
|
|
||||||
|
Boucle de rattrapage qui en résulte : réseau présent → le système lit « plus de
|
||||||
|
soleil » → il conclut qu'il a besoin du réseau → il le garde. La conclusion
|
||||||
|
confirme sa prémisse. **Quatrième oscillateur**, même famille que les trois du
|
||||||
|
24/08.
|
||||||
|
|
||||||
|
### Le correctif
|
||||||
|
Aucun seuil nouveau : le système publiait déjà l'information et personne ne la
|
||||||
|
lisait — l'horodatage de la dernière transition du réseau.
|
||||||
|
|
||||||
|
- **Gabarit snapshot** : `acin1_last_changed_ts`, `grid_switch_last_changed_ts`
|
||||||
|
(`as_timestamp(...last_changed, 0)`, avec garde `is not none` pour une entité
|
||||||
|
absente), plus `grid_settle_seconds` (helper **optionnel**
|
||||||
|
`input_number.evcs_grid_settle_seconds` ; absent → 180 s).
|
||||||
|
⚠️ Un commentaire Jinja `{# #}` est **interdit** à l'intérieur de l'expression
|
||||||
|
`{{ {...} | tojson }}` : `TemplateSyntaxError: unexpected char '#'`.
|
||||||
|
- **INPUT_PARSE** : `parsed.gridTransitionAgeSec` = âge de la **plus récente**
|
||||||
|
des deux transitions (AC-IN physique et contacteur). Horodatage absent ou nul
|
||||||
|
→ `Infinity` → aucune fenêtre, comportement historique. On ne devine jamais
|
||||||
|
une transition qu'on n'a pas observée. `params.gridSettleSeconds` borné [0,900].
|
||||||
|
- **WINDOWS_SOURCEV2** : `derived.gridSettling = gridAvailable && age < seuil`.
|
||||||
|
- **BATTERY_CLOUD_LOCKS** : pendant la fenêtre, `rawSolarStartOk` est faux (on ne
|
||||||
|
démarre pas sur des mesures fausses), les deux termes de mesure de
|
||||||
|
`rawSolarHoldOk` (plancher de décharge, seuil PV) sont neutralisés (on ne laisse
|
||||||
|
pas une charge en cours s'arrêter à cause d'elles), `probeFailedNow` est
|
||||||
|
suspendu (le réseau masque la production, la sonde n'échoue pas) et
|
||||||
|
`solarShouldStepUp` est bloqué. **Les step-down ne sont pas bridés** : c'est la
|
||||||
|
direction sûre.
|
||||||
|
- **HA_SERVICE_CALLS_OUTPUT** : trace `ETABLISSEMENT reseau Xs/Ys` dans la raison,
|
||||||
|
sinon une lecture du logbook pendant le transitoire donne l'impression que le
|
||||||
|
système décide n'importe quoi — alors qu'il décide de ne pas décider.
|
||||||
|
|
||||||
|
### ASYMÉTRIE VOULUE (le point à ne pas casser)
|
||||||
|
La fenêtre ne s'ouvre qu'à l'**arrivée** du réseau. À sa disparition, les
|
||||||
|
onduleurs reprennent instantanément toute la charge : les gardes doivent agir
|
||||||
|
**plus vite, jamais moins vite**. `gridSettling` exige donc `parsed.gridAvailable`.
|
||||||
|
|
||||||
|
De même, la fenêtre suspend le **confort**, jamais le **risque** :
|
||||||
|
`commonSafety*`, planchers SOC, surcharge et arrêt dur batterie restent évalués
|
||||||
|
normalement — application directe de la règle « lissage pour le confort,
|
||||||
|
instantané pour la sécurité ».
|
||||||
|
|
||||||
|
### Golden 42 → 48
|
||||||
|
Chaque cas est apparié à son **contrefactuel hors fenêtre** ; sans cet
|
||||||
|
appariement le test ne prouverait rien (première rédaction : les trois scénarios
|
||||||
|
donnaient le même résultat, le maintien passant par la tolérance nuageuse).
|
||||||
|
|
||||||
|
| scénario | attendu |
|
||||||
|
|---|---|
|
||||||
|
| `etablissement-maintien-charge` (30 s, PV 600 W, batt −15 A) | `charging_step_down_battery` 14 A — continue |
|
||||||
|
| `etablissement-termine-arret-normal` (400 s, idem) | `stop_conditions_lost` |
|
||||||
|
| `etablissement-securite-non-suspendue` (30 s, batt −40 A) | `battery_current_hard_stop` |
|
||||||
|
| `etablissement-ouverture-aucune-fenetre` (réseau disparu 30 s) | `stop_conditions_lost` |
|
||||||
|
| `etablissement-pas-de-demarrage` (30 s, PV 4,5 kW) | `idle_wait_conditions` |
|
||||||
|
| `etablissement-demarrage-hors-fenetre` (400 s, idem) | `start_min_current` |
|
||||||
|
|
||||||
|
Les 42 scénarios existants sont **inchangés** : leur fixture ne porte pas les
|
||||||
|
horodatages, donc `Infinity` → aucune fenêtre. Le repli est sûr par construction.
|
||||||
|
|
||||||
|
Preuve par mutation : `gridSettling = false` → échec des deux scénarios qui
|
||||||
|
testent le correctif (`maintien-charge`, `pas-de-demarrage`), les quatre autres
|
||||||
|
restant verts puisqu'ils n'en dépendent pas. Restauration → 48/48.
|
||||||
|
|
||||||
|
### Suite prévue (ordre convenu avec le propriétaire)
|
||||||
|
1. ✅ fenêtre d'établissement ;
|
||||||
|
2. séquence d'ouverture — baisser la consigne de la borne **avant** d'ouvrir le
|
||||||
|
contacteur, jamais après. Aujourd'hui l'hystérésis ouvre et le contrôleur
|
||||||
|
EVCS l'apprend jusqu'à 30 s plus tard, pendant lesquelles les deux MultiPlus
|
||||||
|
portent la voiture au courant majoré (jusqu'à 28 A en boost) plus la maison :
|
||||||
|
c'est le mécanisme du W-105 déjà consigné ;
|
||||||
|
3. priorité solaire pour la voiture à batterie pleine (coût marginal nul :
|
||||||
|
6-7 trackers sur 7 bridés = ressource refusée) et interdiction de fermer le
|
||||||
|
contacteur pour charger une voiture en journée batterie pleine (~150 Wh).
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis à jour le : 31/08/2026 (~18:20) — fenêtre d'établissement déployée dans src/, golden 48/48, PUT n8n en attente de validation*
|
||||||
|
|
||||||
|
## 30. Session du 31/08/2026 (suite) — Séquence d'ouverture du contacteur
|
||||||
|
|
||||||
|
### Le défaut, en deux morceaux
|
||||||
|
Le plafond de courant EVCS majoré (P5, `input_number.evcs_plafond_courant_edf_on`,
|
||||||
|
jusqu'à **28 A en BOOST**) n'est légitime que tant que le réseau alimente AC-IN :
|
||||||
|
hors réseau, les deux MultiPlus portent seuls la voiture **plus** la maison.
|
||||||
|
C'est le mécanisme du W-105 déjà consigné (§3, maintenance du 09/07).
|
||||||
|
|
||||||
|
1. **Personne ne baissait avant l'ouverture.** C'est l'hystérésis (cycle 1 min)
|
||||||
|
qui ouvre le contacteur ; le contrôleur EVCS ne l'apprenait qu'après coup, via
|
||||||
|
`parsed.acInAvailable`. Les deux automates n'ont aucune poignée de main.
|
||||||
|
2. **Et quand il l'apprenait, il ne baissait pas vite.** La branche
|
||||||
|
`charging_step_down_system_cap` de `DECISION_ENGINEV2` exigeait
|
||||||
|
`lifecycle.currentCooldownElapsed` — un compteur de **confort** — et ne
|
||||||
|
descendait que de `currentStepDownA` par cycle. De 28 A à 20 A : plusieurs
|
||||||
|
cooldowns. L'exposition ne se comptait donc pas en secondes mais en minutes.
|
||||||
|
|
||||||
|
### Le correctif
|
||||||
|
**Aucune poignée de main entre les deux automates, aucun helper nouveau.** Le
|
||||||
|
plafond majoré exige désormais que **l'EVCS demande lui-même le réseau** :
|
||||||
|
|
||||||
|
```js
|
||||||
|
const evcsGridCeilingAllowed =
|
||||||
|
parsed.acInAvailable &&
|
||||||
|
(sourceMode === 'grid_forced' || sourceMode === 'grid_hc_allowed' || forceGridRequested);
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ordonnancement devient mécanique : la demande de l'EVCS tombe au cycle où la
|
||||||
|
fenêtre se ferme (30 s), alors que l'hystérésis ne peut ouvrir qu'**ensuite**
|
||||||
|
(sa branche `turn_off_recovered` exige justement que l'override ait disparu, puis
|
||||||
|
min_on écoulé, sur un cycle d'une minute). L'EVCS baisse donc toujours avant,
|
||||||
|
sans que rien ne soit coordonné.
|
||||||
|
|
||||||
|
⚠️ **Le calcul du plafond a été DÉPLACÉ après le calcul de `sourceMode`** dans
|
||||||
|
`WINDOWS_SOURCEV2` (juste avant `const operatingMode = profile;`). Vérifié avant
|
||||||
|
de bouger : `physicalCapA`, `canRunAtMinCurrent` et les quatre variables
|
||||||
|
intermédiaires n'étaient utilisés qu'à leur définition et dans l'export — rien
|
||||||
|
entre les deux ne les lisait. Ne pas les remonter.
|
||||||
|
|
||||||
|
Second volet, dans `DECISION_ENGINEV2` : **un plafond n'est pas une cible qu'on
|
||||||
|
approche, c'est une limite qu'on respecte.** La réduction ne dépend plus du
|
||||||
|
cooldown et descend directement au plafond (`decision: 'step_down_fast'`, valeur
|
||||||
|
déjà utilisée par la branche surcharge). Réduire est toujours sûr ; c'est la
|
||||||
|
montée qui doit être temporisée, et elle l'est.
|
||||||
|
|
||||||
|
### Portée réelle, et ce qui reste exposé
|
||||||
|
Avec le helper à sa valeur par défaut (20 A), **le comportement est strictement
|
||||||
|
inchangé** : le plafond valait déjà 20 A dans les deux branches. Le correctif ne
|
||||||
|
mord que sur le BOOST 28 A, c'est-à-dire exactement le cas dangereux.
|
||||||
|
|
||||||
|
Reste irréductible : une ouverture **manuelle** (EDF URGENCE) ou une perte
|
||||||
|
physique du réseau ne se laissent pas anticiper. L'exposition y passe de
|
||||||
|
plusieurs minutes à **≤ 30 s plus le temps de rampe de la borne**, ce qui est le
|
||||||
|
minimum atteignable sans signal matériel.
|
||||||
|
|
||||||
|
### Golden 48 → 51
|
||||||
|
| scénario | attendu |
|
||||||
|
|---|---|
|
||||||
|
| `ouverture-plafond-tombe-hors-demande-reseau` (AC-IN présent, BOOST 28, matin solaire, cooldown NON écoulé) | `charging_step_down_system_cap` → **20 A immédiatement** |
|
||||||
|
| `ouverture-plafond-maintenu-en-demande-reseau` (CONTREFACTUEL, `force_grid_now`) | `charging_hold_grid` **28 A**, BOOST préservé |
|
||||||
|
| `ouverture-reseau-disparu-reduction-immediate` (AC-IN absent, cooldown non écoulé) | réduction immédiate à 20 A |
|
||||||
|
|
||||||
|
Preuve par mutation, les deux volets séparément :
|
||||||
|
- plafond majoré reconditionné au seul `acInAvailable` → 1 échec ciblé ;
|
||||||
|
- réduction remise sous `currentCooldownElapsed` → 2 échecs ciblés ;
|
||||||
|
- restauration → 51/51.
|
||||||
|
|
||||||
|
### Suite
|
||||||
|
3. priorité solaire pour la voiture à batterie pleine (coût marginal nul :
|
||||||
|
6-7 trackers sur 7 bridés = ressource refusée) et interdiction de fermer le
|
||||||
|
contacteur pour charger une voiture en journée batterie pleine (~150 Wh).
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis à jour le : 31/08/2026 (~19:00) — séquence d'ouverture corrigée, golden 51/51, PUT n8n en attente de validation*
|
||||||
|
|
||||||
|
## 31. Session du 01/09/2026 (nuit) — La consigne réseau appartient désormais au contrôleur
|
||||||
|
|
||||||
|
### L'incident : charge HC démarrée puis morte en soixante secondes
|
||||||
|
Fenêtre HC ouverte normalement, tout notre pilotage a fait son travail :
|
||||||
|
|
||||||
|
| 01:24:03-04 | latch hystérésis ON, EDF fermé, AC-IN présent |
|
||||||
|
| 01:24:00 | takeover nuit ON, source `grid_hc_allowed` (P7) |
|
||||||
|
| 01:25:15 | ACM en `evcs_takeover_hold`, DVCC à 2 A |
|
||||||
|
| 01:25:30 | `start_grid_hc_min_current` — la charge démarre |
|
||||||
|
| 01:25:42 | le réseau prend la voiture : import **1 450 W** |
|
||||||
|
| 01:26:33 | import retombé à 763 W puis **négatif**, batterie de −7 à **−25 A** |
|
||||||
|
| 01:26:30 | garde « batterie trop sollicitée (−21,5 A) » → arrêt + verrou 17 min |
|
||||||
|
| 01:44 → 01:47 | `battery_current_hard_stop` puis `idle_wait_conditions` |
|
||||||
|
|
||||||
|
### Cause : l'ESS n'utilise le réseau que si on le lui demande
|
||||||
|
`ac_power_setpoint` = **50 W** (corrigé le 31/08 depuis 4 200 W),
|
||||||
|
`ess_min_soc_limit` = 20 %, SOC = 50 % ⇒
|
||||||
|
`ess_batterylife_state` = `self_consumption_soc_above_min`. Au-dessus du SOC
|
||||||
|
minimal, l'ESS fait servir la batterie et ignore le réseau. Le pic à 1 450 W
|
||||||
|
était le transitoire de reprise, pas le régime : en 50 s le régulateur a rendu
|
||||||
|
la charge à la batterie en convergeant vers les 50 W.
|
||||||
|
|
||||||
|
Preuve indépendante, à 01:49 sans voiture : contacteur fermé, maison 761 W,
|
||||||
|
tuile Réseau « – », **batterie −803 W**. Le réseau était branché et inerte.
|
||||||
|
|
||||||
|
**Le garde a eu raison** : la pente était monotone, sa fin était « la batterie
|
||||||
|
porte la voiture toute la nuit ». Le neutraliser aurait été pire.
|
||||||
|
|
||||||
|
**Et la consigne à 4 200 W du propriétaire était PORTEUSE** : c'est elle qui
|
||||||
|
faisait fonctionner toute la mécanique de prise de relais nocturne (P4/P7).
|
||||||
|
J'avais écrit en la retirant que le besoin couvert restait entier et devait être
|
||||||
|
repris ailleurs ; je ne l'avais pas repris. Règle qui en découle, désormais en
|
||||||
|
mémoire : ne jamais retirer un de ces contournements avant d'avoir mis son
|
||||||
|
remplaçant adaptable en production.
|
||||||
|
|
||||||
|
### Correctif (déployé 01/09 02:10, golden 56/56)
|
||||||
|
Le contrôleur écrit lui-même `number.gx_device_ac_power_setpoint`.
|
||||||
|
|
||||||
|
- **Snapshot** : `grid_setpoint_w`, `grid_setpoint_idle_w` (helper optionnel
|
||||||
|
`input_number.evcs_grid_setpoint_idle_w`, absent → 50 W, le défaut Victron).
|
||||||
|
- **INPUT_PARSE** : `parsed.gridSetpointW`, `params.gridSetpointIdleW` [0, 500].
|
||||||
|
- **HA_SERVICE_CALLS_OUTPUT** :
|
||||||
|
`gridSessionActive = acInAvailable && sourceMode ∈ {grid_forced, grid_hc_allowed}`
|
||||||
|
— donc P7 nuit, P9 départ et `force_grid_now`, et rien d'autre.
|
||||||
|
Besoin = `houseNonEvcsW + physicalCapA × acVoltageRef` : une grandeur
|
||||||
|
**mesurée**, pas une constante. Hors session, retour à la valeur de repos.
|
||||||
|
- **BUILD_ACTIONS** : appel `grid_power_setpoint` (`number.set_value`).
|
||||||
|
- Trace `Consigne reseau=XW` dans la raison pendant une session.
|
||||||
|
|
||||||
|
**La bande d'écriture est ASYMÉTRIQUE, et c'est le point.** Une consigne trop
|
||||||
|
basse fait porter la différence par la batterie — c'est le défaut de cette nuit,
|
||||||
|
corrigé sans attendre. Une consigne trop haute ne coûte rien : l'ESS ne prélève
|
||||||
|
que ce qu'il peut placer. On écrit donc avec une marge d'**un pas de courant
|
||||||
|
EVCS** (`currentStepUpA × acVoltageRef` ≈ 230 W — grandeur du système, pas un
|
||||||
|
chiffre inventé) et on ne redescend qu'au-delà de deux marges. Première version
|
||||||
|
sans cette asymétrie : réécriture toutes les 30 s en poursuivant le bruit de la
|
||||||
|
maison. Après : **deux écritures en 25 minutes**.
|
||||||
|
|
||||||
|
⚠️ **Deux leviers, deux rôles, à ne pas confondre** : la consigne dit ce que le
|
||||||
|
réseau a le DROIT de servir ; **DVCC (Auto Charge Manager) dit ce que la batterie
|
||||||
|
a le droit de prendre**. C'est DVCC à 2 A qui empêche une consigne généreuse de
|
||||||
|
gaver la batterie pendant un `evcs_takeover_hold`, et c'est DVCC à 30 A qui
|
||||||
|
autorise la recharge pendant un `evcs_takeover_recover`. Le couple se compose
|
||||||
|
correctement, mais la générosité de la consigne DÉPEND de ce cap : ne pas
|
||||||
|
supprimer l'un sans revoir l'autre.
|
||||||
|
|
||||||
|
### Golden 51 → 56
|
||||||
|
`summarize()` expose `gridSetpointW` (valeur écrite ce cycle, `null` si aucune
|
||||||
|
écriture). Scénarios : session HC → 5 525 W · contrefactuel hors session → 50 W ·
|
||||||
|
consigne déjà généreuse (5 600) → aucune écriture · **consigne 45 W sous le
|
||||||
|
besoin → correction immédiate** (le défaut de la nuit) · dérive externe à
|
||||||
|
4 000 W hors session → ramenée à 50 W.
|
||||||
|
Preuve par mutation : consigne figée au repos → 2 échecs ciblés ; branche
|
||||||
|
« trop basse » neutralisée → 1 échec ciblé ; restauration → 56/56.
|
||||||
|
|
||||||
|
### Vérification en réel
|
||||||
|
02:10 → 02:13, contacteur fermé : grid 4 350 W, **batterie ~0 A**, EVCS 3 500 W,
|
||||||
|
SOC stable à 49 %, consigne écrite à 5 276 puis 5 616 W. La voiture charge sur
|
||||||
|
le réseau en HC sans toucher à la réserve — ce que P7 était censé faire depuis
|
||||||
|
le 07/08 et qui ne tenait que par un réglage manuel.
|
||||||
|
|
||||||
|
### Risque résiduel assumé
|
||||||
|
Si le contrôleur s'arrête en pleine session (SPOF Synology, cf. §3), la consigne
|
||||||
|
reste haute et la substitution de journée peut revenir. C'est le même mode de
|
||||||
|
défaillance que le reste de la chaîne, mais il est nouveau pour ce paramètre :
|
||||||
|
à reprendre le jour où le SPOF sera traité.
|
||||||
|
|
||||||
|
### Non fait volontairement
|
||||||
|
La fenêtre d'établissement (§29) ne couvre que le chemin solaire ; les gardes
|
||||||
|
réseau ne sont pas gardés. Ce n'est pas la cause de cet incident et, la consigne
|
||||||
|
étant désormais correcte, le transitoire observé au redémarrage de 01:58 était
|
||||||
|
de 20 s avec la batterie à −1,6 A. À traiter séparément si un cas se présente.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis à jour le : 01/09/2026 (~02:15) — consigne de puissance réseau pilotée par le contrôleur, golden 56/56, charge HC vérifiée en réel*
|
||||||
|
|
||||||
|
## 32. Session du 01/09/2026 (nuit) — État réel de la résilience, faits vérifiés
|
||||||
|
|
||||||
|
Relevés avant toute proposition de secours (second Synology 10.10.10.22).
|
||||||
|
|
||||||
|
### Ce qui est déjà hors du NAS principal
|
||||||
|
`/mnt/projects` est monté depuis **10.10.10.22** : le dépôt git (src/nodes,
|
||||||
|
golden, tools, docs) ne dépend donc PAS de 10.10.10.17. Gitea vit sur la même
|
||||||
|
machine. La moitié du travail de résilience est faite sans qu'on l'ait cherché.
|
||||||
|
`/mnt/ARCH3/docker` (HA + n8n) est sur **10.10.10.17** uniquement, non répliqué.
|
||||||
|
|
||||||
|
### Aucune sauvegarde Home Assistant n'existe
|
||||||
|
Pas de dossier `backups/` dans `/mnt/ARCH3/docker/HA` (vérifié : seuls `SAVE/`
|
||||||
|
et `SAVE_CONFIG/`, créés à la main lors de nos sessions). La sauvegarde intégrée
|
||||||
|
de HA n'a jamais produit d'archive ici.
|
||||||
|
|
||||||
|
### ⚠️ La clé de chiffrement n8n tient dans 56 octets et n'a aucune copie
|
||||||
|
`N8N_ENCRYPTION_KEY` est **absent de `compose.yaml`** : n8n l'a générée au
|
||||||
|
premier démarrage et elle vit uniquement dans `/mnt/ARCH3/docker/n8n/data/config`
|
||||||
|
(56 octets, `encryptionKey`, 06/02/2026). Sans ce fichier, une copie parfaite de
|
||||||
|
`database.sqlite` (1,69 Go) ne redonne **aucun credential** — OpenRouter compris.
|
||||||
|
C'est l'objet le plus fragile de toute l'installation.
|
||||||
|
|
||||||
|
### Volumétrie, et c'est elle qui commande la stratégie
|
||||||
|
| | taille | nature |
|
||||||
|
|---|---:|---|
|
||||||
|
| `.storage` + `packages` + `dashboards` + yaml | **~6 Mo** | identité : entités, helpers, cartes |
|
||||||
|
| `custom_components` | 52 Mo | réinstallable (HACS), versions à figer |
|
||||||
|
| `home-assistant_v2.db` | 4,39 Go | historique |
|
||||||
|
| `n8n/data/database.sqlite` | 1,69 Go | workflows (déjà en git) + credentials |
|
||||||
|
|
||||||
|
Les deux bases sont en **mode WAL** (`-wal`/`-shm` présents) : un `cp` à chaud
|
||||||
|
sur NFS produit une sauvegarde corrompue **en silence**. Il faut
|
||||||
|
`sqlite3 .backup` / `VACUUM INTO`, ou conteneur arrêté.
|
||||||
|
|
||||||
|
### ⚠️ Le contacteur EDF est un Tuya CLOUD — SPOF distinct du NAS
|
||||||
|
`switch.edf_general_ok_switch` : `platform: tuya`,
|
||||||
|
`unique_id: tuya.bf3d7928e069594e5fzfi9switch` (intégration `tuya`, projet
|
||||||
|
cloud `gg-1060...`, pas `localtuya`). Ouvrir ou fermer le réseau **passe donc
|
||||||
|
par internet**. Or les coupures d'orage de ce site emportent souvent la
|
||||||
|
connexion : le contacteur peut devenir incommandable exactement quand il faut
|
||||||
|
agir, et le tableau « EDF Urgence » avec lui.
|
||||||
|
|
||||||
|
### En revanche, le GX est une troisième machine, indépendante des deux NAS
|
||||||
|
Intégration `victron_gx` = **Venus OS `c0619ab34ac5` sur 10.10.10.9:1883**, et
|
||||||
|
le broker MQTT est sur cette même adresse. Consigne de puissance réseau, DVCC,
|
||||||
|
courant EVCS, arrêt de la borne, SOC, PV, états MPPT : tout est lisible et
|
||||||
|
écrivable **en MQTT local, sans HA et sans cloud**. C'est là-dessus qu'un chien
|
||||||
|
de garde doit s'appuyer — pas sur HA, et surtout pas sur Tuya.
|
||||||
|
|
||||||
|
### Backlog levé sans qu'on le remarque : Météo-France est installée
|
||||||
|
`meteo_france` — Pleumeur-Bodou, Bretagne (22).
|
||||||
|
`sensor.meteo_france_alert_for_department_22_22_weather_alert` = `Vert`, et la
|
||||||
|
réserve d'orage (§26) tourne : `storm_level` vert, réserve **4,95 kWh**
|
||||||
|
(SOC 55 %), manque 0,9 kWh au SOC actuel de 49 %. L'action utilisateur en
|
||||||
|
attente depuis le 27/08 est donc caduque.
|
||||||
|
|
||||||
|
### Risque nouveau introduit le 01/09
|
||||||
|
Le contrôleur écrit maintenant la consigne réseau (§31). S'il s'arrête en pleine
|
||||||
|
session, elle reste haute et la substitution de journée revient. Ce risque
|
||||||
|
n'existait pas avant cette nuit : il justifie à lui seul un chien de garde.
|
||||||
|
|
||||||
|
## 33. Session du 01/09/2026 (nuit) — Chien de garde EVCS sur le second Synology
|
||||||
|
|
||||||
|
### Ce qu'il protège
|
||||||
|
Le contrôleur pilote depuis §31 la consigne de puissance réseau de l'ESS. S'il
|
||||||
|
s'arrête **en pleine session**, elle reste haute et le réseau se substitue au
|
||||||
|
solaire toute la journée suivante. Ce risque est né cette nuit ; le chien de
|
||||||
|
garde existe d'abord pour lui. Il couvre au passage l'incident du 09/07
|
||||||
|
(NAS éteint la nuit, EDF laissé fermé jusqu'au matin).
|
||||||
|
|
||||||
|
### Le signal de vie : surtout PAS un helper Home Assistant
|
||||||
|
Piège trouvé en le construisant, et vérifié : **HA ne rafraîchit pas
|
||||||
|
l'horodatage d'un `input_text` réécrit à l'identique.** À 02:52,
|
||||||
|
`victron_evcs_surplus_state` paraissait figé depuis 2 440 s alors que le
|
||||||
|
contrôleur tournait normalement (`charging_hold_grid` stable depuis 02:12, et la
|
||||||
|
raison tronquée à 255 caractères perdait justement les champs variables). Un
|
||||||
|
helper HA est un mauvais battement de cœur.
|
||||||
|
|
||||||
|
Signal retenu : **l'API d'exécutions de n8n**
|
||||||
|
(`GET /api/v1/executions?workflowId=...&limit=3`). Elle prouve que la boucle
|
||||||
|
s'exécute, et son absence couvre indifféremment « n8n arrêté », « NAS éteint »
|
||||||
|
et « réseau coupé ». Zéro modification du contrôleur, zéro helper à créer.
|
||||||
|
Cas supplémentaire traité : trois exécutions consécutives en `error` = le
|
||||||
|
contrôleur tourne à vide (typiquement HA inaccessible) → considéré mort.
|
||||||
|
|
||||||
|
### Ce qu'il fait, et surtout ce qu'il ne fait pas
|
||||||
|
- Il ne prend **aucune** décision d'énergie : ni seuil, ni démarrage. Il remet
|
||||||
|
l'état sûr par défaut (consigne → 50 W, borne arrêtée) et se tait.
|
||||||
|
- Il ne touche **pas** au contacteur EDF. C'est un Tuya *cloud* (§32), donc un
|
||||||
|
actionneur non fiable en secours ; et ce n'est pas nécessaire : contacteur
|
||||||
|
fermé + consigne au repos = autoconsommation normale, la maison passe sur la
|
||||||
|
batterie jusqu'au SOC minimal puis le réseau reprend seul. État sûr.
|
||||||
|
- Il n'utilise **ni HA ni le cloud** pour agir : MQTT direct sur le GX Venus
|
||||||
|
(10.10.10.9, portail `c0619ab34ac5`), troisième machine indépendante des deux
|
||||||
|
NAS. Topics vérifiés en lecture avant tout code :
|
||||||
|
`settings/0/Settings/CGwacs/AcPowerSetPoint`, `evcharger/40/StartStop`,
|
||||||
|
`evcharger/40/Ac/Power`, `battery/512/Soc`, `system/0/Ac/Grid/L1/Power`.
|
||||||
|
Broker sans authentification, keepalive `R/<portail>/keepalive` obligatoire.
|
||||||
|
- **Une seule action par panne**, puis verrou jusqu'au retour d'un signe de vie.
|
||||||
|
Sans ce verrou, une coupure réseau entre les deux NAS ferait rejouer l'action
|
||||||
|
à chaque cycle pendant que le contrôleur, vivant, la défait : un oscillateur
|
||||||
|
de plus, dans un projet qui en a déjà corrigé quatre.
|
||||||
|
|
||||||
|
### Réglages
|
||||||
|
Seuil de silence **600 s** (20 cycles manqués) : assez long pour ignorer un
|
||||||
|
hoquet, assez court pour qu'une panne nocturne ne coûte pas une journée de
|
||||||
|
production. Période 60 s. Consigne de repos 50 W (défaut Victron).
|
||||||
|
|
||||||
|
### Tests passés
|
||||||
|
1. Contrôleur vivant → aucune écriture, une ligne de veille par heure.
|
||||||
|
2. Panne simulée (`--simulate-dead --dry-run`) → détection, deux écritures
|
||||||
|
annoncées et supprimées, message d'alerte complet avec l'état du GX.
|
||||||
|
3. Verrou : 5 cycles de panne consécutifs → **1 alerte, 2 écritures**, puis
|
||||||
|
silence.
|
||||||
|
|
||||||
|
### Déploiement (action utilisateur)
|
||||||
|
`deploy/watchdog/compose.yaml`, à lancer **sur 10.10.10.22 uniquement** :
|
||||||
|
```
|
||||||
|
cd /volume2/projects/EVCS/EVCS/deploy/watchdog
|
||||||
|
cp watchdog.env.exemple watchdog.env # y coller la clé API n8n
|
||||||
|
docker compose up -d && docker compose logs -f
|
||||||
|
```
|
||||||
|
Le dépôt est monté en **lecture seule** ; seul le journal est écrit, hors dépôt.
|
||||||
|
`node_modules` (14 Mo, `mqtt` 5.15.2) est **sans dépendance native** — le
|
||||||
|
répertoire installé depuis le poste fonctionne tel quel dans le conteneur
|
||||||
|
alpine, et il vit déjà physiquement sur 10.10.10.22 (`/mnt/projects` = export
|
||||||
|
`10.10.10.22:/volume2/projects`).
|
||||||
|
|
||||||
|
⚠️ **Ne jamais le lancer sur 10.10.10.17** : un chien de garde hébergé par la
|
||||||
|
machine qu'il surveille meurt avec elle.
|
||||||
|
|
||||||
|
### Ce qu'il ne couvre pas encore
|
||||||
|
Le contacteur EDF reste incommandable si internet tombe (Tuya cloud). Un
|
||||||
|
contacteur local — ou un `localtuya` — est le prochain vrai gain de résilience.
|
||||||
|
La sauvegarde (6 Mo d'identité vers Gitea, 6,1 Go d'historique en hebdomadaire
|
||||||
|
avec `sqlite3 .backup`) et le secours froid n8n restent à faire.
|
||||||
|
|
||||||
|
## 34. Session du 01/09/2026 (nuit) — Sauvegarde Home Assistant : elle n'existait pas, elle existe
|
||||||
|
|
||||||
|
### Le point de départ
|
||||||
|
Aucune sauvegarde HA n'avait jamais été faite (§32). Et **non, git ne la
|
||||||
|
couvrait pas** : le dépôt contient le code du contrôleur, pas Home Assistant.
|
||||||
|
Un seul fichier HA y était versionné (`dashboards/bilan_energetique.yaml`).
|
||||||
|
Les helpers, les entités, les paquets, les cartes, les intégrations n'avaient
|
||||||
|
aucune copie nulle part.
|
||||||
|
|
||||||
|
### La trouvaille qui a simplifié le problème
|
||||||
|
`backup/agents/info` déclare **quatre destinations**, et le croisement avec les
|
||||||
|
entrées `synology_dsm` de `.storage/core.config_entries` donne leur hôte réel —
|
||||||
|
le nom seul ne dit pas où chacune écrit :
|
||||||
|
|
||||||
|
| agent | nom | hôte |
|
||||||
|
|---|---|---|
|
||||||
|
| `backup.local` | local | 10.10.10.17 |
|
||||||
|
| `synology_dsm.1660NZN396007` | ARCHIVE2 | **10.10.10.22** |
|
||||||
|
| `synology_dsm.14B0MPN914514` | ARCHIVE | **10.10.10.50** |
|
||||||
|
| `synology_dsm.20A0SBRVM9PF2` | ARCHIVE3 | 10.10.10.17 ⚠️ à ne PAS utiliser |
|
||||||
|
|
||||||
|
Home Assistant sait donc **pousser lui-même ses sauvegardes vers deux autres
|
||||||
|
machines**. Aucun script de recopie, aucune tâche planifiée, aucun montage NFS
|
||||||
|
supplémentaire : il n'y avait qu'à le configurer. (Troisième machine 10.10.10.50
|
||||||
|
découverte au passage — elle n'apparaissait dans aucun document du projet.)
|
||||||
|
|
||||||
|
### Configuration appliquée (`tools/ha-backup-config.mjs`, rejouable)
|
||||||
|
La configuration de sauvegarde **n'est pas exposée par l'API REST** : elle vit
|
||||||
|
dans `.storage/backup` et ne se règle que par l'interface ou le **websocket**.
|
||||||
|
Le script passe par le websocket, ce qui la rend reproductible et relisible.
|
||||||
|
|
||||||
|
- **Destinations** : local + 10.10.10.22 + 10.10.10.50. ARCHIVE3 (10.10.10.17)
|
||||||
|
volontairement exclu : une sauvegarde qui ne survit pas à la panne qu'elle
|
||||||
|
couvre ne protège rien.
|
||||||
|
- **Base de données EXCLUE.** Ce qu'il faut restaurer vite, c'est l'identité du
|
||||||
|
système — 20 Mo compressés. L'historique pèse 4,4 Go, change en permanence et
|
||||||
|
n'a aucune valeur d'urgence : il se sauvegarde à part, plus rarement, et avec
|
||||||
|
les précautions SQLite (mode WAL, jamais un `cp` à chaud).
|
||||||
|
- **Quotidienne à 04:45**, rétention **7 copies**.
|
||||||
|
- **Sans mot de passe**, choix assumé : ces archives restent sur le réseau local
|
||||||
|
du propriétaire, et une sauvegarde restaurable vaut mieux qu'une sauvegarde
|
||||||
|
chiffrée dont la clé est perdue le jour où elle sert.
|
||||||
|
|
||||||
|
### Vérifiée, pas seulement déclenchée
|
||||||
|
Première archive : `Automatic backup 2026.8.1` (a9d35ac9), 01/09 03:11,
|
||||||
|
**20,3 Mo, présente sur les trois machines**. Contenu contrôlé archive en main :
|
||||||
|
`.storage` 39 fichiers · `packages` 18 · `dashboards` 30 ·
|
||||||
|
`custom_components` 2 283 · `configuration.yaml`, `secrets.yaml`,
|
||||||
|
`automations.yaml`, `scripts.yaml` · et **zéro occurrence** de
|
||||||
|
`home-assistant_v2.db`, comme voulu.
|
||||||
|
`tools/ha-backup-list.mjs` affiche pour chaque sauvegarde les hôtes où elle est
|
||||||
|
réellement présente — c'est cette colonne-là qui compte.
|
||||||
|
|
||||||
|
### Ce que cette sauvegarde ne couvre PAS
|
||||||
|
1. **n8n.** Ni ses workflows (déjà en git, donc couverts autrement), ni sa base
|
||||||
|
`database.sqlite` (1,69 Go), ni surtout **la clé de chiffrement de 56 octets**
|
||||||
|
(§32) sans laquelle aucun credential n'est récupérable. C'est le prochain
|
||||||
|
trou à boucher, et le plus petit.
|
||||||
|
2. **L'historique HA** (4,4 Go) : perdu en cas de restauration. Conséquence
|
||||||
|
réelle pour le chantier du bilan énergétique, qui s'appuie sur les
|
||||||
|
statistiques long terme. À traiter séparément, en hebdomadaire.
|
||||||
|
|
||||||
|
### Validation de bout en bout, nuit du 01/09 (fin de session)
|
||||||
|
|
||||||
|
**Chemin d'écriture du chien de garde DÉMONTRÉ**, pas seulement simulé
|
||||||
|
(`tools/watchdog-selftest.mjs`, rejouable) : écriture réelle d'une sonde de
|
||||||
|
777 W sur `W/<portail>/settings/0/Settings/CGwacs/AcPowerSetPoint`, relecture
|
||||||
|
conforme, remise de la valeur d'origine par le script lui-même. Le `--dry-run`
|
||||||
|
supprimait justement ce maillon-là ; sans cette preuve, le chien de garde
|
||||||
|
n'était que probablement fonctionnel. Test fait EDF ouvert, donc consigne
|
||||||
|
inerte : risque nul.
|
||||||
|
|
||||||
|
**Et la séquence de fin de session s'est déroulée dans le bon ordre**, sans
|
||||||
|
que personne l'ait orchestrée :
|
||||||
|
|
||||||
|
| 03:31:30 | fin de charge → `night_recovery_done_idle`, et **la consigne retombe à 50 W dans le même cycle** |
|
||||||
|
| 03:32:04 | 34 s plus tard, l'hystérésis ouvre le contacteur (`turn_off_recovery_done`) |
|
||||||
|
|
||||||
|
La demande baisse AVANT que le réseau parte — le principe de la séquence
|
||||||
|
d'ouverture (§30), obtenu ici par construction et non par coordination. C'est
|
||||||
|
la garantie que le risque introduit en §31 (consigne laissée haute) ne se
|
||||||
|
matérialise pas en fonctionnement normal ; le chien de garde ne couvre plus que
|
||||||
|
le cas de la panne.
|
||||||
|
|
||||||
|
**Bilan de la nuit HC** : charge démarrée à 01:58 après correction, menée à son
|
||||||
|
terme à 03:31 sans jamais entamer la réserve (batterie tenue entre −2 et +1 A
|
||||||
|
pendant toute la session), SOC stable à 49 %.
|
||||||
|
|
||||||
|
**Clé API n8n** : désormais aussi dans `.env` à la racine (convention du
|
||||||
|
propriétaire), en plus de `~/.config/opencode/opencode.json`. Le repli du chien
|
||||||
|
de garde sur `.env` est vérifié — utile pour tout déploiement hors poste de
|
||||||
|
travail.
|
||||||
|
|||||||
45
deploy/watchdog/compose.yaml
Normal file
45
deploy/watchdog/compose.yaml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Chien de garde EVCS — à déployer sur le SECOND Synology (10.10.10.22).
|
||||||
|
#
|
||||||
|
# Il surveille le contrôleur qui tourne sur 10.10.10.17 et, s'il se tait,
|
||||||
|
# remet la consigne de puissance réseau de l'ESS à sa valeur de repos et
|
||||||
|
# arrête la borne — en MQTT direct sur le GX Venus (10.10.10.9), donc sans
|
||||||
|
# passer par Home Assistant ni par le cloud Tuya.
|
||||||
|
#
|
||||||
|
# ⚠️ NE JAMAIS le lancer sur 10.10.10.17 : un chien de garde hébergé par la
|
||||||
|
# machine qu'il surveille meurt avec elle.
|
||||||
|
#
|
||||||
|
# Mise en service :
|
||||||
|
# cd /volume2/projects/EVCS/EVCS/deploy/watchdog
|
||||||
|
# cp watchdog.env.exemple watchdog.env puis y coller la clé API n8n
|
||||||
|
# docker compose up -d
|
||||||
|
# docker compose logs -f
|
||||||
|
#
|
||||||
|
# Vérification sans risque, avant de le laisser en service :
|
||||||
|
# docker compose run --rm evcs-watchdog node tools/watchdog-evcs.mjs --once
|
||||||
|
# docker compose run --rm evcs-watchdog node tools/watchdog-evcs.mjs --once --dry-run --simulate-dead
|
||||||
|
|
||||||
|
services:
|
||||||
|
evcs-watchdog:
|
||||||
|
image: node:22-alpine
|
||||||
|
container_name: evcs-watchdog
|
||||||
|
restart: always
|
||||||
|
working_dir: /app
|
||||||
|
network_mode: bridge
|
||||||
|
volumes:
|
||||||
|
# Le dépôt en LECTURE SEULE : le chien de garde n'a aucune raison
|
||||||
|
# d'écrire dans le code qu'il exécute.
|
||||||
|
- /volume2/projects/EVCS/EVCS:/app:ro
|
||||||
|
# Son journal, seul point d'écriture, hors du code.
|
||||||
|
# ⚠️ Le dossier ./logs doit EXISTER avant `up` : le démon Docker de
|
||||||
|
# Synology DSM refuse un bind-mount dont le dossier hôte est absent,
|
||||||
|
# là où Docker standard le créerait. Il est versionné vide pour ça.
|
||||||
|
- ./logs:/var/log/evcs-watchdog:rw
|
||||||
|
env_file:
|
||||||
|
- watchdog.env
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Paris
|
||||||
|
- WATCHDOG_LOG=/var/log/evcs-watchdog/watchdog-evcs.log
|
||||||
|
command: ["node", "tools/watchdog-evcs.mjs"]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "process.exit(0)"]
|
||||||
|
interval: 5m
|
||||||
14
deploy/watchdog/watchdog.env.exemple
Normal file
14
deploy/watchdog/watchdog.env.exemple
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Copier en `watchdog.env` (exclu de git) et renseigner la clé.
|
||||||
|
# La clé API n8n se trouve dans ~/.config/opencode/opencode.json
|
||||||
|
# (mcp.n8n-mcp.environment.N8N_API_KEY) sur le poste de travail.
|
||||||
|
N8N_API_KEY=
|
||||||
|
|
||||||
|
# Tout le reste a des valeurs par défaut correctes ; à ne changer qu'en
|
||||||
|
# connaissance de cause.
|
||||||
|
# N8N_URL=https://n8n.airtria.pro
|
||||||
|
# EVCS_WORKFLOW_ID=ATswvGfG4KJUls0K
|
||||||
|
# GX_MQTT_HOST=10.10.10.9
|
||||||
|
# GX_PORTAL_ID=c0619ab34ac5
|
||||||
|
# WATCHDOG_MAX_AGE_SEC=600 # 20 cycles manqués avant de conclure à la panne
|
||||||
|
# WATCHDOG_PERIOD_SEC=60
|
||||||
|
# WATCHDOG_IDLE_SETPOINT_W=50 # défaut Victron
|
||||||
@@ -599,3 +599,129 @@ Ordre de travail qui en découle, avant tout câblage :
|
|||||||
- [ ] **États zombies** `stop_source_lost` / `idle_wait_source` — noms
|
- [ ] **États zombies** `stop_source_lost` / `idle_wait_source` — noms
|
||||||
transitoires peu lisibles dans le journal et le tableau de bord (cf. HANDOFF
|
transitoires peu lisibles dans le journal et le tableau de bord (cf. HANDOFF
|
||||||
§24, hors périmètre à l'époque).
|
§24, hors périmètre à l'époque).
|
||||||
|
|
||||||
|
## 01/09/2026 — La frontière de l'excédent, mesurée sur 8 jours
|
||||||
|
|
||||||
|
Question du propriétaire : faut-il charger la voiture le soir ? Avec la règle
|
||||||
|
qu'il pose et qui gouverne désormais le sujet — **la voiture est un délestage de
|
||||||
|
l'excédent PV, pas une charge prioritaire qui vide les batteries solaires**, et
|
||||||
|
la frontière se détecte à la **puissance de décharge batterie**, pas à la
|
||||||
|
production PV ni à un seuil de SOC.
|
||||||
|
|
||||||
|
### Méthode
|
||||||
|
|
||||||
|
`tools/etude-frontiere-excedent.mjs` intègre l'historique HA (résolution native
|
||||||
|
~2 s, rétention 90 jours effective depuis le 24/08 — vérifié) et répartit
|
||||||
|
l'énergie **réellement livrée au véhicule**, instant par instant, entre trois
|
||||||
|
origines physiques :
|
||||||
|
|
||||||
|
```
|
||||||
|
batterie : min(P_evcs, décharge batterie) -> réserve entamée
|
||||||
|
réseau : min(reste, import réseau) -> énergie payante
|
||||||
|
excédent : le solde -> production qui aurait été
|
||||||
|
refusée ou simplement stockée
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune écriture, aucune décision : le script lit et intègre. Convention
|
||||||
|
d'attribution assumée : sans la voiture, la décharge batterie aurait été
|
||||||
|
réduite d'au plus `P_evcs` — l'attribution est donc une borne haute honnête de
|
||||||
|
ce que la voiture a coûté à la réserve.
|
||||||
|
|
||||||
|
### Ce que les 8 jours disent (24/08 → 31/08)
|
||||||
|
|
||||||
|
| | kWh | part |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Livré au véhicule | 80,13 | 100 % |
|
||||||
|
| dont **excédent** | 51,18 | **64 %** |
|
||||||
|
| dont **batterie** | 13,22 | **16 %** |
|
||||||
|
| dont réseau (HC nuit, voulu) | 15,73 | 20 % |
|
||||||
|
|
||||||
|
Part d'excédent par heure, cumul sur la période :
|
||||||
|
|
||||||
|
| h | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | **19** | **20** |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| part excédent | 94 % | 99 % | 86 % | 80 % | 83 % | 90 % | 77 % | 92 % | 81 % | **38 %** | **4 %** |
|
||||||
|
|
||||||
|
**La frontière existe, elle est franche, et elle tombe entre 18 h 30 et 19 h 50**
|
||||||
|
(heure du dernier instant où le véhicule chargeait sans faire décharger la
|
||||||
|
batterie) :
|
||||||
|
|
||||||
|
| jour | fermeture | SOC max du jour |
|
||||||
|
|---|---|---:|
|
||||||
|
| 24/08 | 19:26 | 95 % |
|
||||||
|
| 25/08 | 19:29 | 95 % |
|
||||||
|
| 28/08 | 19:50 | 93 % |
|
||||||
|
| 29/08 | 18:47 | 94 % |
|
||||||
|
| 30/08 | 18:31 | 94 % |
|
||||||
|
| 31/08 | 20:06 (0 % d'excédent) | 95 % |
|
||||||
|
|
||||||
|
### Pourquoi la charge de journée, elle, est gratuite
|
||||||
|
|
||||||
|
La batterie **sature à 93-96 % tous les jours**. L'énergie détournée vers la
|
||||||
|
voiture entre 10 h et 18 h n'a donc coûté aucune réserve : le plateau était
|
||||||
|
atteint de toute façon, et la production au-delà était refusée (§ censure). Ce
|
||||||
|
n'est vrai que *tant que la batterie sature* — c'est le second terme de la
|
||||||
|
règle, et c'est lui qui bougera avec la saison.
|
||||||
|
|
||||||
|
### Le coût réel de la charge du soir
|
||||||
|
|
||||||
|
13,22 kWh pris à la réserve sur 8 jours, soit **1,65 kWh/jour**, rachetés au
|
||||||
|
réseau la nuit suivante — avec en plus le rendement aller-retour de la batterie
|
||||||
|
(~10 %) et un cycle de charge/décharge pour rien. Le tarif est le même (HC dans
|
||||||
|
les deux cas) : le surcoût est purement en pertes et en usure. Autrement dit,
|
||||||
|
charger le soir sur la batterie **revient à charger la voiture au réseau, en
|
||||||
|
plus cher et en abîmant le parc**.
|
||||||
|
|
||||||
|
### La deuxième question : combien garder pour la nuit ?
|
||||||
|
|
||||||
|
Elle est déjà résolue, et pas par un seuil. Le bilan publie
|
||||||
|
`evcs_balance_next_deficit_kwh` et `..._required_soc` — l'énergie de la
|
||||||
|
prochaine traversée déficitaire et le niveau à atteindre avant. Validation du
|
||||||
|
26/08 : point bas de la nuit prédit **à un point de SOC près sur douze heures**.
|
||||||
|
|
||||||
|
Mesure à 18 h, SOC requis pour franchir la nuit **sans réseau** :
|
||||||
|
|
||||||
|
| 24/08 | 25/08 | 26/08 | 27/08 | 28/08 | 29/08 | 30/08 | 31/08 |
|
||||||
|
|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| 113,7 % | 119,4 % | 22,3 % ⚠️ | 114,3 % | 112,9 % | 112,3 % | 109,5 % | 117,9 % |
|
||||||
|
|
||||||
|
**Sept jours sur huit, même pleine, la batterie ne passe pas la nuit.** Déficit
|
||||||
|
structurel de 10 à 20 points, soit 1,5 à 3 kWh — c'est `storage_shortfall_kwh`,
|
||||||
|
le chiffre du dimensionnement, et il concorde avec les 11,8 kWh/jour jetés faute
|
||||||
|
de puits. L'import de nuit n'est donc pas évitable aujourd'hui ; ce qui est
|
||||||
|
évitable, c'est de l'**aggraver** en vidant la réserve le soir.
|
||||||
|
⚠️ Le 22,3 % du 26/08 est à vérifier (statut du bilan ce jour-là) avant d'être
|
||||||
|
cité.
|
||||||
|
|
||||||
|
### La règle qui en découle — deux termes, tous deux mesurés
|
||||||
|
|
||||||
|
```
|
||||||
|
charger le véhicule <=> (a) la batterie ne se décharge pas pour le nourrir
|
||||||
|
ET (b) elle atteindra quand même le niveau exigé par la
|
||||||
|
prochaine traversée déficitaire
|
||||||
|
```
|
||||||
|
|
||||||
|
- **(a)** est instantané, physique, déjà mesuré (`battery_power`), et se pilote
|
||||||
|
en asservissement : monter le courant tant que la batterie ne décharge pas,
|
||||||
|
redescendre dès qu'elle décharge. Ce n'est pas un seuil, c'est un servo.
|
||||||
|
- **(b)** est le bilan, déjà calculé toutes les 5 minutes
|
||||||
|
(`nextDeficit.reachable`). C'est lui qui portera la saison : quand les jours
|
||||||
|
raccourciront, la batterie cessera de saturer et (b) se fermera de lui-même,
|
||||||
|
de plus en plus tôt — sans qu'aucune constante n'ait à être touchée.
|
||||||
|
|
||||||
|
Ce couple remplace `batteryAssistSolarStartOk` (SOC ≥ 90), `soc_start_min`,
|
||||||
|
`midday_soc_min` et les planchers de profil : tous sont des mandataires de « la
|
||||||
|
production est refusée », justes à midi et faux au crépuscule.
|
||||||
|
|
||||||
|
### L'étude longue : rien à instrumenter
|
||||||
|
|
||||||
|
Constat utile : **l'historique HA suffit**. 2 s de résolution sur les puissances,
|
||||||
|
90 jours de rétention, et le bilan qui publie ses helpers toutes les 5 minutes
|
||||||
|
depuis le 24/08. La grandeur à suivre semaine après semaine est l'**heure de
|
||||||
|
fermeture de la frontière** et le **SOC max du jour** ; 8 jours ne montrent
|
||||||
|
aucune dérive saisonnière exploitable — il en faut plusieurs semaines, ce qui
|
||||||
|
est précisément l'objet de l'étude.
|
||||||
|
|
||||||
|
Bénéfice second, non cherché : les mêmes relevés donnent la courbe
|
||||||
|
énergie ↔ point de SOC par bande, c'est-à-dire le point (a) de l'ordre de
|
||||||
|
travail du 26/08, qui bloque tout câblage de la cible du matin.
|
||||||
|
|||||||
303
docs/PROCEDURE_ESS_PARALLELE.md
Normal file
303
docs/PROCEDURE_ESS_PARALLELE.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# Procédure — corriger la substitution EDF↔solaire sur les 2 MultiPlus-II en parallèle
|
||||||
|
|
||||||
|
> Rédigé le 30/08/2026, avant intervention. À suivre dans l'ordre : les étapes
|
||||||
|
> sont classées par **risque croissant**, et il est très possible que tout soit
|
||||||
|
> réglé avant d'arriver à VEConfigure.
|
||||||
|
>
|
||||||
|
> Rappel du symptôme à corriger (prouvé le 27/08, protocole alterné
|
||||||
|
> `docs/mesures/20260827-test-edf-mppt-alterne.log`) : dès que le contacteur EDF
|
||||||
|
> se ferme, la production solaire tombe de ~3 900 W à ~1 050 W et le réseau
|
||||||
|
> reprend la charge au watt près, à consommation inchangée. Quatre fois de
|
||||||
|
> suite, transitions en 75-90 s. Perte : **73 % de la production**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 0 — Trois lectures gratuites (à faire AVANT tout le reste)
|
||||||
|
|
||||||
|
Aucune de ces trois vérifications ne touche à VEConfigure. Deux d'entre elles
|
||||||
|
sont des réglages du GX, modifiables depuis l'écran ou la console à distance,
|
||||||
|
réversibles en dix secondes.
|
||||||
|
|
||||||
|
### 0.1 — La consigne de puissance réseau est à 4200 W (défaut Victron : 50 W)
|
||||||
|
|
||||||
|
**C'est le suspect numéro un.** Relevé le 30/08 dans Home Assistant :
|
||||||
|
|
||||||
|
```
|
||||||
|
number.gx_device_ac_power_setpoint = 4200 W (défaut : 50 W)
|
||||||
|
number.airtria_mpii_v1_ac_power_setpoint_l1 = 4200 W
|
||||||
|
```
|
||||||
|
|
||||||
|
C'est le paramètre ESS *Grid setpoint* (`/Settings/CGwacs/AcPowerSetPoint`) :
|
||||||
|
**la puissance que le système s'efforce en permanence de prélever sur le
|
||||||
|
réseau** quand AC-IN est présent. À 4200 W, on ne demande pas au système de
|
||||||
|
s'équilibrer sur le solaire : on lui demande d'importer 4,2 kW.
|
||||||
|
|
||||||
|
Et comme l'ESS régule *la puissance au point de raccordement*, il n'a qu'un
|
||||||
|
levier pour faire de la place à cet import quand les puits (maison + batterie)
|
||||||
|
sont déjà servis : **brider les chargeurs solaires via DVCC**. D'où la
|
||||||
|
substitution, et d'où le résidu remarquablement constant.
|
||||||
|
|
||||||
|
Les deux campagnes de mesure se réconcilient exactement avec cette hypothèse :
|
||||||
|
|
||||||
|
| Test | Conso | PV pendant EDF ON | Réseau | Vérification |
|
||||||
|
|---|---:|---:|---:|---|
|
||||||
|
| 27/08 10:47, SOC 71 % | 877 W | — | monte à **4113 W** | ≈ la consigne 4200 W, le surplus part en batterie (le courant batterie s'effondre de 104,6 → 25,7 A à la coupure) |
|
||||||
|
| 27/08 11:38, SOC 86 % | 2258 W | 1043 W | 2287 W | batterie en absorption, elle n'accepte plus que ~1070 W : `2258 + 1070 − 1043 = 2285` ✔ |
|
||||||
|
|
||||||
|
Dans le second cas la consigne de 4200 W n'est pas atteignable (la batterie
|
||||||
|
sature), mais le système fait tout ce qu'il peut dans cette direction — et ce
|
||||||
|
« tout ce qu'il peut » consiste précisément à effacer le solaire.
|
||||||
|
|
||||||
|
**Vérification (aucun risque)** — console GX / Remote Console :
|
||||||
|
`Paramètres → ESS → Point de consigne réseau`. Confirmer qu'il affiche bien
|
||||||
|
4200 W. *Ne pas conclure sur le seul nom de l'entité HA : il faut le voir à
|
||||||
|
l'écran du GX.*
|
||||||
|
|
||||||
|
**Correction** : ramener à **50 W**. Valeur par défaut Victron, légèrement
|
||||||
|
positive pour garantir qu'on n'injecte jamais — ce qui est exactement le
|
||||||
|
régime voulu ici.
|
||||||
|
- soit sur le GX : même menu ;
|
||||||
|
- soit depuis HA : `number.gx_device_ac_power_setpoint` → 50.
|
||||||
|
|
||||||
|
**Noter la valeur d'origine (4200) avant de changer.** Retour arrière = remettre
|
||||||
|
4200.
|
||||||
|
|
||||||
|
⚠️ Il est très probable que ce 4200 soit un ancien contournement manuel, de la
|
||||||
|
même famille que les trois charges programmées de l'ESS désactivées le 27/08 —
|
||||||
|
mises en place à l'époque où le système ne tenait plus ses seuils. À confirmer
|
||||||
|
de mémoire avant de supposer qu'il s'agit d'un accident.
|
||||||
|
|
||||||
|
### 0.2 — Le SOC minimal de l'ESS est à 40 %, au-dessus de notre cible de nuit
|
||||||
|
|
||||||
|
```
|
||||||
|
number.gx_device_ess_min_soc_limit = 40 %
|
||||||
|
sensor.gx_device_active_soc_limit = 40.0 %
|
||||||
|
```
|
||||||
|
|
||||||
|
En dessous de 40 %, l'ESS **recharge la batterie sur le réseau de sa propre
|
||||||
|
initiative**, sans rien demander à personne. Ce n'est pas une hypothèse :
|
||||||
|
l'historique montre l'état `recharge` deux nuits sur les sept derniers jours
|
||||||
|
(29/08 04:08, 30/08 01:21), précédé à chaque fois de
|
||||||
|
`soc_below_battery_life_dynamic_soc_limit`.
|
||||||
|
|
||||||
|
Or notre contrôleur vise **35 %** le matin en été. **Deux automates avec deux
|
||||||
|
consignes contradictoires sur la même batterie** : le nôtre relâche à 35, le GX
|
||||||
|
reprend à 40. C'est le même cas de figure que les charges programmées.
|
||||||
|
|
||||||
|
Ce n'est pas la cause de la substitution (les deux tests étaient à SOC 71 % et
|
||||||
|
86 %), mais c'est une source d'import réseau non commandé qu'il faut trancher.
|
||||||
|
|
||||||
|
**Décision à prendre** (pas d'urgence, pas de risque) : soit descendre ce
|
||||||
|
plancher sous la cible du contrôleur, soit — plus cohérent avec la direction du
|
||||||
|
projet — le laisser comme **filet de sécurité bas** (par exemple 20 %) et
|
||||||
|
laisser le bilan énergétique piloter la vraie cible. À arbitrer ensemble ;
|
||||||
|
je ne le change pas sans votre accord.
|
||||||
|
|
||||||
|
### 0.3 — L'assistant ESS tourne bien, et il n'y a aucune erreur VE.Bus
|
||||||
|
|
||||||
|
Deux indices concordants, relevés le 30/08 :
|
||||||
|
- `sensor.gx_device_ess_batterylife_state` produit des états vivants et
|
||||||
|
cohérents (`self_consumption`, `self_consumption_soc_exceeds_85`, `recharge`) —
|
||||||
|
la boucle de régulation ESS **tourne**, elle n'est pas absente ;
|
||||||
|
- toutes les alarmes VE.Bus sont à `no_alarm`, aucune erreur d'installation
|
||||||
|
n'est signalée.
|
||||||
|
|
||||||
|
**Conséquence** : l'hypothèse « l'assistant ESS a été perdu ou dépareillé entre
|
||||||
|
les deux unités lors du passage en parallèle » est **affaiblie** — sans être
|
||||||
|
éliminée, car un assistant présent mais mal paramétré ne lève aucune alarme.
|
||||||
|
C'est ce qui justifie de garder la partie VEConfigure ci-dessous en réserve,
|
||||||
|
mais **en seconde intention**.
|
||||||
|
|
||||||
|
### 0.4 — Le test de validation (30 minutes, à faire après 0.1)
|
||||||
|
|
||||||
|
Ne pas se fier à une impression. Rejouer **exactement** le protocole alterné du
|
||||||
|
27/08, qui a déjà fait ses preuves :
|
||||||
|
|
||||||
|
- OFF 3 min / ON 3 min / OFF 3 min / ON 3 min, échantillon toutes les 15 s ;
|
||||||
|
- grandeurs relevées : PV, courant **et tension** batterie, conso, import
|
||||||
|
réseau, limite DVCC, mode ACM, modes de fonctionnement des MPPT.
|
||||||
|
- Conditions : plein soleil, batterie **non pleine** (SOC < 80 % de préférence),
|
||||||
|
et de quoi consommer (chauffe-eau en marche par exemple).
|
||||||
|
|
||||||
|
Critère de réussite, sans ambiguïté possible : **la PV ne bouge pas d'une phase
|
||||||
|
à l'autre**, et le réseau reste proche de zéro pendant les phases ON.
|
||||||
|
|
||||||
|
L'alternance est indispensable : elle interdit à une dérive météo de se faire
|
||||||
|
passer pour un effet, ce qui avait ruiné la première campagne du 27/08 à 10:47.
|
||||||
|
|
||||||
|
Je peux relancer ce test dès que vous me dites que le setpoint est corrigé.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 1 — VEConfigure, seulement si l'étape 0 n'a pas suffi
|
||||||
|
|
||||||
|
### 1.1 — Ce qu'il ne faut surtout PAS faire
|
||||||
|
|
||||||
|
1. **Ne jamais relancer le VE.Bus System Configurator.** C'est l'outil qui
|
||||||
|
déclare le nombre d'unités et les phases. Le relancer **efface la
|
||||||
|
configuration des deux onduleurs** et impose de tout refaire. On vient
|
||||||
|
modifier des réglages, on ne reconstruit pas le système.
|
||||||
|
2. **Ne pas mettre à jour le firmware dans la foulée.** Voir §2.
|
||||||
|
3. **Ne pas travailler sans sauvegarde préalable** (§1.3).
|
||||||
|
4. **Ne pas enchaîner plusieurs corrections.** Une modification, une
|
||||||
|
vérification, puis la suivante — sinon on ne saura pas laquelle a agi.
|
||||||
|
|
||||||
|
### 1.2 — Matériel et accès : deux chemins possibles
|
||||||
|
|
||||||
|
**Chemin A — Remote VEConfigure via VRM (recommandé, pas de câble)**
|
||||||
|
- Prérequis : le GX est en ligne sur VRM, et la **communication bidirectionnelle**
|
||||||
|
est activée (VRM → Paramètres de l'installation).
|
||||||
|
- VRM → votre installation → *Liste des appareils* → **Remote VEConfigure**.
|
||||||
|
- « Télécharger le fichier de réglages » : VRM interroge le GX, qui lit **les
|
||||||
|
deux unités** et produit un fichier de configuration **système** (extension
|
||||||
|
`.rvms` pour un système multi-unités, `.rvsc` pour une unité seule).
|
||||||
|
- Ce fichier s'ouvre dans **VEConfigure3** (Windows — paquet *VE Configuration
|
||||||
|
Tools*, téléchargeable gratuitement sur le site Victron). Un sélecteur permet
|
||||||
|
de passer de l'unité maître à l'unité parallèle.
|
||||||
|
- Puis « Envoyer et appliquer le fichier de réglages ».
|
||||||
|
|
||||||
|
**Chemin B — Câble MK3-USB (si VRM n'est pas accessible)**
|
||||||
|
- Interface **MK3-USB** branchée sur un port VE.Bus (RJ45) de **l'onduleur
|
||||||
|
maître** ; VEConfigure3 détecte le système complet, pas une unité isolée.
|
||||||
|
|
||||||
|
⚠️ **Dans les deux cas, l'écriture des réglages provoque un redémarrage bref du
|
||||||
|
VE.Bus** : les charges de la maison sont coupées quelques secondes. À faire à un
|
||||||
|
moment où c'est acceptable, et surtout **pas** pendant une charge de véhicule.
|
||||||
|
|
||||||
|
### 1.3 — Sauvegarde (obligatoire, avant toute écriture)
|
||||||
|
|
||||||
|
1. Télécharger le fichier de réglages **et le conserver tel quel**, sans
|
||||||
|
l'ouvrir en écriture : c'est le point de retour.
|
||||||
|
2. Le nommer explicitement, par exemple
|
||||||
|
`SAVE_CONFIG/vebus-parallele-AVANT-20260830.rvms`, et le mettre à l'abri
|
||||||
|
**hors du GX et hors du NAS** (le Synology est déjà un point unique de
|
||||||
|
défaillance connu du projet).
|
||||||
|
3. Photographier ou noter, depuis la console GX : point de consigne réseau,
|
||||||
|
SOC minimal, mode ESS, limite de courant d'entrée CA. Ces valeurs-là ne sont
|
||||||
|
pas dans le fichier VEConfigure — elles vivent dans le GX.
|
||||||
|
|
||||||
|
### 1.4 — Inspection en lecture seule (rien ne s'écrit à cette étape)
|
||||||
|
|
||||||
|
Ouvrir le fichier dans VEConfigure3 et **relever**, pour **chacune des deux
|
||||||
|
unités** (basculer avec le sélecteur d'appareil) :
|
||||||
|
|
||||||
|
| Onglet | Point à vérifier | Ce qu'on cherche |
|
||||||
|
|---|---|---|
|
||||||
|
| **Assistants** | Liste des assistants installés | **ESS présent sur les DEUX unités**, même version. Un assistant présent sur le maître seul est la panne classique après un passage en parallèle. |
|
||||||
|
| **Assistants → ESS** (rejouer l'assistant en lecture) | « Y a-t-il un compteur réseau ? » | Doit correspondre à la réalité. Si l'assistant croit qu'un compteur externe existe alors qu'il n'y en a pas (ou qu'il a été désaffecté lors du passage en parallèle), la boucle de régulation lit une puissance réseau fausse et se comporte exactement comme observé. |
|
||||||
|
| **Assistants → ESS** | Type de batterie / tensions / *sustain* | Cohérence avec les Pylontech pilotées en CAN-bus. |
|
||||||
|
| **Grid** | Code réseau / pays | Un code réseau réinitialisé après reconstruction du parallèle peut imposer des comportements non voulus. |
|
||||||
|
| **Inverter** | PowerAssist, limite de courant d'entrée CA | Doit être cohérent entre les deux unités. |
|
||||||
|
| **Virtual switch** | Doit être **désactivé** | L'assistant ESS et le *virtual switch* sont incompatibles. |
|
||||||
|
| **Charger** | Tensions de charge | Largement outrepassé par DVCC avec une batterie managée — à noter, pas à corriger en priorité. |
|
||||||
|
|
||||||
|
**Toute divergence entre les deux unités est un résultat en soi** : la noter
|
||||||
|
avant de toucher à quoi que ce soit.
|
||||||
|
|
||||||
|
### 1.5 — Corrections, une seule à la fois
|
||||||
|
|
||||||
|
Ordre recommandé, du plus probable au moins probable :
|
||||||
|
1. assistant ESS manquant ou dépareillé entre les deux unités ;
|
||||||
|
2. réponse « compteur réseau » de l'assistant ESS incohérente avec l'installation ;
|
||||||
|
3. limite de courant d'entrée CA / PowerAssist incohérentes.
|
||||||
|
|
||||||
|
Après **chaque** écriture : attendre le retour du VE.Bus, vérifier que les
|
||||||
|
alarmes restent à `no_alarm`, puis **rejouer le test §0.4**. Si le comportement
|
||||||
|
ne change pas, revenir au fichier de sauvegarde avant d'essayer la correction
|
||||||
|
suivante — on ne veut pas empiler des modifications non concluantes.
|
||||||
|
|
||||||
|
### 1.6 — Retour arrière
|
||||||
|
|
||||||
|
Renvoyer le fichier `…-AVANT-20260830.rvms` par le même chemin (VRM ou MK3),
|
||||||
|
et restaurer à la main les réglages GX notés en §1.3. C'est pour cela qu'il faut
|
||||||
|
les avoir notés : ils ne sont pas dans le fichier.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 2 — Mise à jour du firmware : à ne pas faire maintenant
|
||||||
|
|
||||||
|
Si « mise à jour » désignait le **firmware** des MultiPlus-II, la réponse reste
|
||||||
|
celle du 27/08 : **aucune des mesures effectuées ne justifie une mise à jour**.
|
||||||
|
Les raisons de ne pas la faire dans le même mouvement :
|
||||||
|
|
||||||
|
- sur un système en parallèle, **les deux unités doivent porter exactement le
|
||||||
|
même firmware** ; une divergence casse le parallèle ;
|
||||||
|
- une mise à jour firmware peut réinitialiser des réglages et **imposer de
|
||||||
|
refaire la configuration du système** — c'est précisément l'opération à
|
||||||
|
risque qu'on cherche à éviter ;
|
||||||
|
- on ne diagnostique pas deux variables à la fois : si l'étape 0 corrige le
|
||||||
|
symptôme, une mise à jour n'aura servi qu'à brouiller la piste.
|
||||||
|
|
||||||
|
Si elle devient nécessaire un jour, elle se fait **avec accès physique aux
|
||||||
|
onduleurs**, jamais à distance, et après la sauvegarde du §1.3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Journal d'intervention (à remplir au fur et à mesure)
|
||||||
|
|
||||||
|
| Date/heure | Action | Valeur avant | Valeur après | Résultat du test §0.4 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 30/08 21:46 | Consigne réseau GX | 4200 W | **50 W** | à faire (plein soleil) |
|
||||||
|
| 30/08 ~21:55 | Limite de charge (% de CCL) — *paramètre obsolète* | 95 % | **100 %** | remis au défaut |
|
||||||
|
| 30/08 ~21:55 | Limite de décharge (% de DCL) — *paramètre obsolète* | 20 % | **100 %** | remis au défaut |
|
||||||
|
| 30/08 22:0x | SOC minimal ESS | 40 % | **20 %** | ⚠ limite *active* encore à 40 % (BatteryLife) |
|
||||||
|
| | (VEConfigure, si nécessaire) | | | non entamé |
|
||||||
|
|
||||||
|
### Note sur l'épisode « paramètres obsolètes » (30/08)
|
||||||
|
|
||||||
|
Les lignes « Limite de charge/décharge de la batterie (% de CCL / % de DCL) »
|
||||||
|
de l'écran ESS ne sont **pas des limites de SOC** : ce sont des pourcentages des
|
||||||
|
courants que la batterie déclare accepter et fournir (*Charge/Discharge Current
|
||||||
|
Limit*). Portées à 95 % / 20 %, elles auraient plafonné la décharge à 20 % de
|
||||||
|
150 A = **30 A ≈ 1,5 kW**, là où la maison avait tiré 73,7 A le soir même et
|
||||||
|
108 A dans l'après-midi. Réseau ouvert — le régime habituel ici — la batterie
|
||||||
|
n'aurait plus pu servir la maison ; réseau fermé, l'écart serait revenu au
|
||||||
|
réseau, soit exactement la substitution qu'on cherche à supprimer.
|
||||||
|
Victron les range lui-même sous « Paramètres obsolètes » : DVCC (`forced_on`
|
||||||
|
avec une batterie managée en CAN-bus) en est le vrai propriétaire.
|
||||||
|
**Valeur correcte : 100 % / 100 %.**
|
||||||
|
|
||||||
|
### Note sur le SOC minimal et BatteryLife (30/08)
|
||||||
|
|
||||||
|
`MinimumSocLimit` a été ramené de 40 % à **20 %**, pour en faire un filet de
|
||||||
|
sécurité bas cohérent avec les seuils du contrôleur (plancher dur 22 %,
|
||||||
|
hystérésis réseau ON à 20 %) au lieu d'un concurrent de la cible du matin
|
||||||
|
(35 % en été).
|
||||||
|
|
||||||
|
⚠️ **La limite qui agit réellement n'a pas suivi immédiatement** :
|
||||||
|
`sensor.gx_device_active_soc_limit` reste à 40 %. Avec BatteryLife actif, le GX
|
||||||
|
maintient une limite *dynamique* propre, qui monte de 5 % quand la batterie
|
||||||
|
passe sous elle et ne redescend que de 5 % par jour de charge complète.
|
||||||
|
L'historique le montre sans ambiguïté : 40 → 45 chaque nuit, 45 → 40 chaque
|
||||||
|
jour, jamais en dessous de 40, sept jours durant.
|
||||||
|
|
||||||
|
Deux options :
|
||||||
|
1. **attendre** : la limite descendra 40 → 35 → 30 → 25 → 20 au rythme d'un
|
||||||
|
palier par journée de charge complète, soit environ quatre jours ;
|
||||||
|
2. **basculer le mode ESS** sur « Optimisé (sans BatteryLife) » depuis l'écran
|
||||||
|
GX (ligne *Mode*, au-dessus de *État BatteryLife*) : la limite active devient
|
||||||
|
alors immédiatement égale au minimum réglé.
|
||||||
|
|
||||||
|
L'option 2 est la plus cohérente avec la direction du projet — une limite
|
||||||
|
dynamique cachée que rien ne lit est précisément le type d'acteur non lu qu'on
|
||||||
|
est en train d'éliminer.
|
||||||
|
|
||||||
|
**Retenue et appliquée le 30/08 au soir** (mode ESS → « Optimisé sans
|
||||||
|
BatteryLife », depuis l'écran GX : le mode n'est PAS exposé à Home Assistant).
|
||||||
|
Vérification immédiate :
|
||||||
|
|
||||||
|
```
|
||||||
|
sensor.gx_device_active_soc_limit : 40 % → 20 % ✔
|
||||||
|
sensor.gx_device_ess_batterylife_state : self_consumption
|
||||||
|
→ self_consumption_soc_above_min ✔
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Faux ami à connaître : `select.gx_device_dess_mode` (options
|
||||||
|
`off/auto_vrm/buy/sell/node_red`) n'est PAS le mode ESS — c'est le *Dynamic
|
||||||
|
ESS*, piloté par les prix de l'électricité. Il doit rester sur `off`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document créé le 30/08/2026. Relevés HA à l'appui : consigne 4200 W et SOC
|
||||||
|
minimal 40 % lus le 30/08 ; historique `recharge` des 29 et 30/08 ; alarmes
|
||||||
|
VE.Bus toutes à `no_alarm`.*
|
||||||
72
docs/mesures/20260831-1043-test-edf-alterne.log
Normal file
72
docs/mesures/20260831-1043-test-edf-alterne.log
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
TEST ALTERNE EDF <-> MPPT — 31/08/2026 10:43:54
|
||||||
|
phases 180s, echantillon 15s, sequence OFF_1 / ON_1 / OFF_2 / ON_2 / FIN_OFF
|
||||||
|
etat initial : edf off | latch off | hysteresis on
|
||||||
|
consigne reseau 50 W | SOC min ESS 20 % | limite active 20.0 %
|
||||||
|
MPPT ordre: mppt1 rs_1 rs_2 rs_3 rs_4 rs_g v1 (MPPT=cherche le max, lim_=se retient)
|
||||||
|
|
||||||
|
10:43:56 | OFF_1 | PV 7353 | U 50.13 | I 97.3 | conso 2270 | grid 0 | SOC 39 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:44:11 | OFF_1 | PV 6985 | U 50.15 | I 95.8 | conso 2258 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:44:26 | OFF_1 | PV 6710 | U 50.16 | I 91.3 | conso 2276 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:44:41 | OFF_1 | PV 6216 | U 50.12 | I 75.5 | conso 2309 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:44:56 | OFF_1 | PV 6749 | U 50.10 | I 70.7 | conso 2246 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:45:11 | OFF_1 | PV 6683 | U 50.17 | I 91.3 | conso 2263 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:45:26 | OFF_1 | PV 5462 | U 50.10 | I 67.8 | conso 2252 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:45:42 | OFF_1 | PV 5423 | U 50.08 | I 64.4 | conso 2270 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:45:57 | OFF_1 | PV 6697 | U 50.10 | I 74.8 | conso 2268 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:46:12 | OFF_1 | PV 6911 | U 50.12 | I 78.3 | conso 2264 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:46:27 | OFF_1 | PV 7168 | U 50.18 | I 97.2 | conso 2246 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:46:42 | OFF_1 | PV 7498 | U 50.19 | I 96.0 | conso 2272 | grid 0 | SOC 40 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
>>> EDF ON
|
||||||
|
10:47:00 | ON_1 | PV 7278 | U 50.21 | I 98.9 | conso 2252 | grid 0 | SOC 41 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:47:15 | ON_1 | PV 7169 | U 50.23 | I 99.0 | conso 2244 | grid 0 | SOC 41 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:47:30 | ON_1 | PV 7447 | U 50.22 | I 95.3 | conso 2286 | grid 0 | SOC 41 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:47:45 | ON_1 | PV 7327 | U 50.22 | I 94.5 | conso 2300 | grid 0 | SOC 41 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:48:00 | ON_1 | PV 7221 | U 50.22 | I 95.4 | conso 2252 | grid 0 | SOC 41 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:48:15 | ON_1 | PV 7222 | U 50.37 | I 141.1 | conso 2249 | grid 2120 | SOC 41 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
10:48:30 | ON_1 | PV 6884 | U 50.36 | I 129.1 | conso 2245 | grid 1914 | SOC 41 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
10:48:45 | ON_1 | PV 7087 | U 50.37 | I 126.8 | conso 2252 | grid 1727 | SOC 41 | DVCC 130A/51.5V | acin 1 | edf on | volt MPPT MPPT MPPT MPPT volt volt
|
||||||
|
10:49:00 | ON_1 | PV 7331 | U 50.37 | I 126.0 | conso 2309 | grid 1556 | SOC 42 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
10:49:15 | ON_1 | PV 7458 | U 50.37 | I 124.4 | conso 2228 | grid 1248 | SOC 42 | DVCC 130A/51.5V | acin 1 | edf on | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:49:30 | ON_1 | PV 7520 | U 50.37 | I 122.8 | conso 2293 | grid 1117 | SOC 42 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:49:45 | ON_1 | PV 6730 | U 50.35 | I 115.5 | conso 2271 | grid 886 | SOC 42 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
>>> EDF OFF
|
||||||
|
10:50:04 | OFF_2 | PV 7329 | U 50.32 | I 108.6 | conso 2254 | grid 0 | SOC 42 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:50:19 | OFF_2 | PV 6540 | U 50.28 | I 99.0 | conso 2397 | grid 0 | SOC 42 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:50:34 | OFF_2 | PV 7382 | U 50.25 | I 93.0 | conso 2338 | grid 0 | SOC 42 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:50:49 | OFF_2 | PV 7356 | U 50.28 | I 99.0 | conso 2240 | grid 0 | SOC 42 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:51:04 | OFF_2 | PV 7639 | U 50.28 | I 99.8 | conso 2243 | grid 0 | SOC 42 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:51:19 | OFF_2 | PV 7381 | U 50.28 | I 101.0 | conso 2240 | grid 0 | SOC 43 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:51:34 | OFF_2 | PV 7508 | U 50.27 | I 98.2 | conso 2257 | grid 0 | SOC 43 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:51:49 | OFF_2 | PV 7700 | U 50.27 | I 100.2 | conso 2331 | grid 0 | SOC 43 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:52:04 | OFF_2 | PV 7879 | U 50.28 | I 101.6 | conso 2305 | grid 0 | SOC 43 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:52:19 | OFF_2 | PV 7570 | U 50.29 | I 103.3 | conso 2328 | grid 0 | SOC 43 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:52:34 | OFF_2 | PV 7853 | U 50.29 | I 103.3 | conso 2311 | grid 0 | SOC 43 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:52:49 | OFF_2 | PV 7899 | U 50.30 | I 106.8 | conso 2309 | grid 0 | SOC 43 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
>>> EDF ON
|
||||||
|
10:53:07 | ON_2 | PV 6995 | U 50.30 | I 103.0 | conso 2329 | grid 0 | SOC 43 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:53:22 | ON_2 | PV 5384 | U 50.22 | I 80.6 | conso 2822 | grid 0 | SOC 43 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:53:37 | ON_2 | PV 4032 | U 50.01 | I 23.6 | conso 2793 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:53:52 | ON_2 | PV 3607 | U 49.91 | I 6.9 | conso 2825 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:54:07 | ON_2 | PV 3686 | U 49.87 | I 11.4 | conso 2804 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:54:22 | ON_2 | PV 3707 | U 50.03 | I 70.3 | conso 2874 | grid 2717 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:54:37 | ON_2 | PV 3794 | U 50.06 | I 63.8 | conso 2926 | grid 2598 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:54:52 | ON_2 | PV 3037 | U 50.05 | I 57.4 | conso 2719 | grid 2221 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:55:07 | ON_2 | PV 2968 | U 50.01 | I 43.8 | conso 2731 | grid 1956 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:55:22 | ON_2 | PV 3078 | U 49.98 | I 39.8 | conso 2739 | grid 1760 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:55:37 | ON_2 | PV 3603 | U 49.98 | I 43.8 | conso 2767 | grid 1555 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:55:52 | ON_2 | PV 4702 | U 50.00 | I 50.1 | conso 2741 | grid 1343 | SOC 44 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
>>> EDF OFF
|
||||||
|
10:56:11 | FIN_OFF| PV 4788 | U 50.07 | I 65.7 | conso 2664 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:56:26 | FIN_OFF| PV 5765 | U 50.02 | I 51.7 | conso 2682 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:56:41 | FIN_OFF| PV 5249 | U 50.00 | I 44.5 | conso 2634 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:56:56 | FIN_OFF| PV 4821 | U 49.96 | I 36.0 | conso 2664 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:57:11 | FIN_OFF| PV 6658 | U 49.98 | I 45.1 | conso 2642 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:57:26 | FIN_OFF| PV 7882 | U 50.15 | I 93.1 | conso 2644 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 0 | edf off | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:57:41 | FIN_OFF| PV 5740 | U 50.26 | I 114.8 | conso 2360 | grid 0 | SOC 44 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:57:56 | FIN_OFF| PV 5774 | U 50.12 | I 61.2 | conso 2327 | grid 0 | SOC 45 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:58:11 | FIN_OFF| PV 4755 | U 50.10 | I 61.2 | conso 2336 | grid 0 | SOC 45 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:58:26 | FIN_OFF| PV 4824 | U 50.04 | I 48.1 | conso 2350 | grid 0 | SOC 45 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:58:41 | FIN_OFF| PV 5987 | U 50.05 | I 55.6 | conso 2345 | grid 0 | SOC 45 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
10:58:56 | FIN_OFF| PV 6074 | U 50.09 | I 67.5 | conso 2381 | grid 0 | SOC 45 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
>>> RESTAURATION (fin de campagne)
|
||||||
|
>>> etat restaure : edf off | latch off | hysteresis on
|
||||||
92
docs/mesures/20260831-1101-test-edf-alterne.log
Normal file
92
docs/mesures/20260831-1101-test-edf-alterne.log
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
TEST ALTERNE EDF <-> MPPT — 31/08/2026 11:01:00
|
||||||
|
phases 240s, echantillon 15s, sequence OFF_1 / ON_1 / OFF_2 / ON_2 / FIN_OFF
|
||||||
|
etat initial : edf off | latch off | hysteresis on
|
||||||
|
consigne reseau 50 W | SOC min ESS 20 % | limite active 20.0 %
|
||||||
|
MPPT ordre: mppt1 rs_1 rs_2 rs_3 rs_4 rs_g v1 (MPPT=cherche le max, lim_=se retient)
|
||||||
|
|
||||||
|
11:01:02 | OFF_1 | PV 5200 | U 50.09 | I 48.2 | conso 2275 | grid 0 | SOC 46 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:01:17 | OFF_1 | PV 6109 | U 50.11 | I 60.3 | conso 2279 | grid 0 | SOC 46 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:01:32 | OFF_1 | PV 5632 | U 50.19 | I 84.3 | conso 1983 | grid 0 | SOC 46 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:01:47 | OFF_1 | PV 8639 | U 50.35 | I 131.3 | conso 2108 | grid 0 | SOC 46 | DVCC 130A/51.5V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
11:02:02 | OFF_1 | PV 8510 | U 50.38 | I 126.4 | conso 2372 | grid 0 | SOC 46 | DVCC 130A/51.5V | acin 0 | edf off | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:02:18 | OFF_1 | PV 8540 | U 50.33 | I 100.9 | conso 2452 | grid 0 | SOC 46 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:02:33 | OFF_1 | PV 7535 | U 50.30 | I 89.8 | conso 2424 | grid 0 | SOC 47 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:02:48 | OFF_1 | PV 6464 | U 50.30 | I 97.3 | conso 2393 | grid 0 | SOC 47 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:03:03 | OFF_1 | PV 7018 | U 50.22 | I 74.3 | conso 2400 | grid 0 | SOC 47 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:03:18 | OFF_1 | PV 7233 | U 50.28 | I 93.0 | conso 2381 | grid 0 | SOC 47 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:03:33 | OFF_1 | PV 8269 | U 50.32 | I 104.7 | conso 2405 | grid 0 | SOC 47 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:03:48 | OFF_1 | PV 6161 | U 50.29 | I 84.4 | conso 2395 | grid 0 | SOC 47 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:04:03 | OFF_1 | PV 5965 | U 50.20 | I 58.6 | conso 2404 | grid 0 | SOC 47 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:04:18 | OFF_1 | PV 6338 | U 50.22 | I 77.2 | conso 2397 | grid 0 | SOC 47 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:04:33 | OFF_1 | PV 6878 | U 50.22 | I 75.6 | conso 2409 | grid 0 | SOC 47 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:04:48 | OFF_1 | PV 6276 | U 50.20 | I 66.7 | conso 2394 | grid 0 | SOC 48 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
>>> EDF ON
|
||||||
|
11:05:06 | ON_1 | PV 5262 | U 50.16 | I 61.4 | conso 2452 | grid 0 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:05:21 | ON_1 | PV 5674 | U 50.13 | I 57.5 | conso 2458 | grid 0 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:05:36 | ON_1 | PV 5740 | U 50.10 | I 54.3 | conso 2410 | grid 0 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:05:51 | ON_1 | PV 6539 | U 50.18 | I 78.4 | conso 2083 | grid 0 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:06:06 | ON_1 | PV 7018 | U 50.23 | I 80.1 | conso 2107 | grid 0 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:06:21 | ON_1 | PV 5203 | U 50.35 | I 114.4 | conso 2113 | grid 1985 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:06:36 | ON_1 | PV 5126 | U 50.31 | I 93.4 | conso 2107 | grid 1811 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:06:52 | ON_1 | PV 4853 | U 50.29 | I 90.0 | conso 2131 | grid 1603 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:07:07 | ON_1 | PV 4935 | U 50.26 | I 78.6 | conso 2184 | grid 1418 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:07:22 | ON_1 | PV 5147 | U 50.23 | I 74.6 | conso 2140 | grid 1135 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:07:37 | ON_1 | PV 5535 | U 50.26 | I 84.6 | conso 2120 | grid 939 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:07:52 | ON_1 | PV 4455 | U 50.21 | I 62.1 | conso 2079 | grid 659 | SOC 48 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:08:07 | ON_1 | PV 4105 | U 50.14 | I 50.5 | conso 2086 | grid 445 | SOC 49 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:08:22 | ON_1 | PV 4116 | U 50.08 | I 42.0 | conso 2160 | grid 307 | SOC 49 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:08:37 | ON_1 | PV 5153 | U 50.09 | I 50.3 | conso 2070 | grid 25 | SOC 49 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:08:52 | ON_1 | PV 4488 | U 50.09 | I 48.3 | conso 2067 | grid 25 | SOC 49 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
>>> EDF OFF
|
||||||
|
11:09:10 | OFF_2 | PV 4483 | U 50.06 | I 38.3 | conso 2314 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:09:25 | OFF_2 | PV 4953 | U 50.05 | I 41.6 | conso 2305 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:09:40 | OFF_2 | PV 6915 | U 50.10 | I 58.7 | conso 2304 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:09:55 | OFF_2 | PV 6850 | U 50.19 | I 76.7 | conso 2310 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:10:10 | OFF_2 | PV 6174 | U 50.22 | I 76.9 | conso 2309 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:10:25 | OFF_2 | PV 5937 | U 50.16 | I 59.6 | conso 2374 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:10:40 | OFF_2 | PV 6519 | U 50.17 | I 64.4 | conso 2319 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:10:55 | OFF_2 | PV 5987 | U 50.22 | I 74.4 | conso 2340 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:11:10 | OFF_2 | PV 4922 | U 50.15 | I 51.5 | conso 2381 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:11:25 | OFF_2 | PV 5392 | U 50.10 | I 43.9 | conso 2361 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:11:40 | OFF_2 | PV 5504 | U 50.16 | I 62.3 | conso 2347 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:11:55 | OFF_2 | PV 6235 | U 50.17 | I 67.1 | conso 2324 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:12:11 | OFF_2 | PV 7208 | U 50.26 | I 84.7 | conso 2330 | grid 0 | SOC 49 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:12:26 | OFF_2 | PV 5227 | U 50.24 | I 69.9 | conso 2350 | grid 0 | SOC 50 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:12:41 | OFF_2 | PV 4526 | U 50.14 | I 44.0 | conso 2371 | grid 0 | SOC 50 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:12:56 | OFF_2 | PV 3986 | U 50.11 | I 40.1 | conso 4229 | grid 0 | SOC 50 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
>>> EDF ON
|
||||||
|
11:13:14 | ON_2 | PV 4963 | U 50.05 | I 38.8 | conso 2398 | grid 0 | SOC 50 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:13:29 | ON_2 | PV 5666 | U 50.07 | I 41.0 | conso 2369 | grid 0 | SOC 50 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:13:44 | ON_2 | PV 5415 | U 50.14 | I 59.1 | conso 2374 | grid 0 | SOC 50 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:13:59 | ON_2 | PV 6240 | U 50.14 | I 62.6 | conso 2380 | grid 0 | SOC 50 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:14:14 | ON_2 | PV 7346 | U 50.24 | I 87.1 | conso 2635 | grid 0 | SOC 50 | DVCC 130A/51.5V | acin 1 | edf on | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:14:29 | ON_2 | PV 7398 | U 50.47 | I 142.9 | conso 2730 | grid 2600 | SOC 50 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
11:14:44 | ON_2 | PV 6944 | U 50.48 | I 129.5 | conso 2495 | grid 2190 | SOC 50 | DVCC 130A/51.5V | acin 1 | edf on | volt MPPT MPPT MPPT MPPT MPPT volt
|
||||||
|
11:14:59 | ON_2 | PV 7122 | U 50.50 | I 127.0 | conso 2498 | grid 1949 | SOC 51 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
11:15:14 | ON_2 | PV 7312 | U 50.52 | I 126.2 | conso 2540 | grid 1806 | SOC 51 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
11:15:29 | ON_2 | PV 7540 | U 50.53 | I 126.8 | conso 2482 | grid 1536 | SOC 51 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
11:15:44 | ON_2 | PV 7966 | U 50.54 | I 123.9 | conso 2515 | grid 1237 | SOC 51 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
11:15:59 | ON_2 | PV 8017 | U 50.54 | I 121.3 | conso 2503 | grid 1094 | SOC 51 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt MPPT
|
||||||
|
11:16:14 | ON_2 | PV 8456 | U 50.56 | I 123.5 | conso 2499 | grid 828 | SOC 51 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
11:16:29 | ON_2 | PV 8592 | U 50.57 | I 125.9 | conso 2116 | grid 277 | SOC 52 | DVCC 130A/51.5V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
11:16:44 | ON_2 | PV 8488 | U 50.59 | I 126.8 | conso 2089 | grid 52 | SOC 52 | DVCC 130A/51.5V | acin 1 | edf on | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:16:59 | ON_2 | PV 8759 | U 50.57 | I 121.0 | conso 2092 | grid -41 | SOC 52 | DVCC 130A/51.5V | acin 1 | edf on | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
>>> EDF OFF
|
||||||
|
11:17:17 | FIN_OFF| PV 8921 | U 50.58 | I 127.5 | conso 2058 | grid 0 | SOC 52 | DVCC 130A/51.5V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
11:17:32 | FIN_OFF| PV 8483 | U 50.58 | I 126.6 | conso 2054 | grid 0 | SOC 52 | DVCC 130A/51.5V | acin 0 | edf off | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:17:47 | FIN_OFF| PV 8296 | U 50.57 | I 121.9 | conso 2024 | grid 0 | SOC 52 | DVCC 130A/51.5V | acin 0 | edf off | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:18:02 | FIN_OFF| PV 7904 | U 50.57 | I 122.9 | conso 2024 | grid 0 | SOC 53 | DVCC 130A/51.5V | acin 0 | edf off | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:18:17 | FIN_OFF| PV 8785 | U 50.59 | I 128.9 | conso 2025 | grid 0 | SOC 53 | DVCC 130A/51.5V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
11:18:32 | FIN_OFF| PV 8128 | U 50.60 | I 125.2 | conso 2028 | grid 0 | SOC 53 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:18:48 | FIN_OFF| PV 8287 | U 50.61 | I 127.3 | conso 2018 | grid 0 | SOC 53 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:19:03 | FIN_OFF| PV 6198 | U 50.50 | I 90.7 | conso 2021 | grid 0 | SOC 53 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:19:18 | FIN_OFF| PV 8660 | U 50.47 | I 89.1 | conso 2012 | grid 0 | SOC 53 | DVCC 130A/51.5V | acin 0 | edf off | volt volt volt volt MPPT volt MPPT
|
||||||
|
11:19:33 | FIN_OFF| PV 8310 | U 50.58 | I 120.2 | conso 2019 | grid 0 | SOC 54 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:19:48 | FIN_OFF| PV 8015 | U 50.59 | I 118.9 | conso 2317 | grid 0 | SOC 54 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:20:03 | FIN_OFF| PV 8740 | U 50.55 | I 111.1 | conso 2364 | grid 0 | SOC 54 | DVCC 130A/51.5V | acin 0 | edf off | MPPT MPPT MPPT volt volt volt MPPT
|
||||||
|
11:20:18 | FIN_OFF| PV 9047 | U 50.61 | I 121.6 | conso 2392 | grid 0 | SOC 54 | DVCC 130A/51.5V | acin 0 | edf off | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:20:33 | FIN_OFF| PV 9009 | U 50.64 | I 124.3 | conso 2343 | grid 0 | SOC 54 | DVCC 130A/51.5V | acin 0 | edf off | volt MPPT MPPT MPPT MPPT MPPT MPPT
|
||||||
|
11:20:48 | FIN_OFF| PV 9155 | U 50.64 | I 122.4 | conso 2459 | grid 0 | SOC 54 | DVCC 130A/51.5V | acin 0 | edf off | volt MPPT MPPT MPPT MPPT MPPT volt
|
||||||
|
11:21:03 | FIN_OFF| PV 9115 | U 50.66 | I 127.1 | conso 2343 | grid 0 | SOC 54 | DVCC 130A/51.5V | acin 0 | edf off | MPPT volt volt volt MPPT volt volt
|
||||||
|
>>> RESTAURATION (fin de campagne)
|
||||||
|
>>> etat restaure : edf off | latch off | hysteresis on
|
||||||
92
docs/mesures/20260831-1319-test-edf-alterne.log
Normal file
92
docs/mesures/20260831-1319-test-edf-alterne.log
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
TEST ALTERNE EDF <-> MPPT — 31/08/2026 12:58:43
|
||||||
|
phases 240s, echantillon 15s, sequence OFF_1 / ON_1 / OFF_2 / ON_2 / FIN_OFF
|
||||||
|
etat initial : edf off | latch off | hysteresis on
|
||||||
|
consigne reseau 50 W | SOC min ESS 20 % | limite active 20.0 %
|
||||||
|
MPPT ordre: mppt1 rs_1 rs_2 rs_3 rs_4 rs_g v1 (MPPT=cherche le max, lim_=se retient)
|
||||||
|
|
||||||
|
12:58:46 | OFF_1 | PV 3692 | U 50.93 | I 22.6 | conso 2323 | grid 0 | SOC 90 | DVCC 110A/51.2V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
12:59:01 | OFF_1 | PV 3680 | U 50.93 | I 23.3 | conso 2271 | grid 0 | SOC 90 | DVCC 110A/51.2V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
12:59:16 | OFF_1 | PV 3653 | U 50.93 | I 23.5 | conso 2281 | grid 0 | SOC 90 | DVCC 110A/51.2V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
12:59:31 | OFF_1 | PV 3621 | U 50.93 | I 23.0 | conso 2264 | grid 0 | SOC 90 | DVCC 110A/51.2V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
12:59:46 | OFF_1 | PV 3627 | U 50.92 | I 23.1 | conso 2266 | grid 0 | SOC 90 | DVCC 110A/51.2V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:00:01 | OFF_1 | PV 3625 | U 50.92 | I 22.5 | conso 2273 | grid 0 | SOC 90 | DVCC 110A/51.2V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:00:16 | OFF_1 | PV 3611 | U 50.92 | I 22.8 | conso 2266 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:00:31 | OFF_1 | PV 3632 | U 50.92 | I 23.5 | conso 2265 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:00:46 | OFF_1 | PV 3659 | U 50.93 | I 23.0 | conso 2269 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:01:01 | OFF_1 | PV 3649 | U 50.93 | I 22.8 | conso 2270 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:01:16 | OFF_1 | PV 3643 | U 50.94 | I 23.6 | conso 2250 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:01:31 | OFF_1 | PV 3638 | U 50.96 | I 23.2 | conso 2268 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:01:46 | OFF_1 | PV 3675 | U 50.97 | I 22.3 | conso 2290 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:02:01 | OFF_1 | PV 3438 | U 50.97 | I 21.7 | conso 2270 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:02:16 | OFF_1 | PV 3397 | U 50.95 | I 18.3 | conso 2256 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:02:31 | OFF_1 | PV 3377 | U 50.94 | I 17.6 | conso 2378 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
>>> EDF ON
|
||||||
|
13:02:49 | ON_1 | PV 3487 | U 50.93 | I 17.6 | conso 2308 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
13:03:04 | ON_1 | PV 3430 | U 50.92 | I 18.5 | conso 2284 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
13:03:19 | ON_1 | PV 3430 | U 50.92 | I 18.8 | conso 2269 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
13:03:34 | ON_1 | PV 3390 | U 50.93 | I 18.1 | conso 2259 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
13:03:49 | ON_1 | PV 3369 | U 50.93 | I 18.2 | conso 2266 | grid 0 | SOC 90 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
13:04:04 | ON_1 | PV 1890 | U 51.14 | I 42.6 | conso 2245 | grid 2083 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:04:20 | ON_1 | PV 1503 | U 51.11 | I 25.4 | conso 2251 | grid 1915 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | off volt volt volt volt volt volt
|
||||||
|
13:04:35 | ON_1 | PV 1502 | U 51.06 | I 17.9 | conso 2318 | grid 1790 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:04:50 | ON_1 | PV 1809 | U 51.03 | I 16.1 | conso 2328 | grid 1535 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:05:05 | ON_1 | PV 2004 | U 51.01 | I 17.0 | conso 2337 | grid 1343 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:05:20 | ON_1 | PV 2239 | U 50.99 | I 17.2 | conso 2309 | grid 1093 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:05:35 | ON_1 | PV 2401 | U 50.98 | I 16.6 | conso 2320 | grid 866 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
13:05:50 | ON_1 | PV 2698 | U 50.97 | I 17.3 | conso 2300 | grid 637 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
13:06:05 | ON_1 | PV 2846 | U 50.95 | I 16.1 | conso 2332 | grid 462 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
13:06:20 | ON_1 | PV 3411 | U 50.98 | I 17.7 | conso 2317 | grid 244 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
13:06:35 | ON_1 | PV 3315 | U 51.02 | I 16.4 | conso 2326 | grid 8 | SOC 91 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt MPPT volt volt
|
||||||
|
>>> EDF OFF
|
||||||
|
13:06:53 | OFF_2 | PV 3481 | U 51.03 | I 18.1 | conso 2329 | grid 0 | SOC 91 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:07:08 | OFF_2 | PV 3459 | U 51.03 | I 18.3 | conso 2345 | grid 0 | SOC 91 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:07:23 | OFF_2 | PV 3404 | U 51.04 | I 18.1 | conso 2255 | grid 0 | SOC 91 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:07:38 | OFF_2 | PV 3363 | U 51.06 | I 17.9 | conso 2273 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:07:53 | OFF_2 | PV 3409 | U 51.07 | I 17.8 | conso 2299 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:08:08 | OFF_2 | PV 3190 | U 51.12 | I 26.0 | conso 1941 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | off volt volt volt volt volt volt
|
||||||
|
13:08:23 | OFF_2 | PV 2974 | U 51.14 | I 20.6 | conso 1896 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | off volt volt volt MPPT volt volt
|
||||||
|
13:08:38 | OFF_2 | PV 3016 | U 51.15 | I 19.3 | conso 1895 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt MPPT volt volt
|
||||||
|
13:08:53 | OFF_2 | PV 2975 | U 51.16 | I 19.0 | conso 1916 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:09:08 | OFF_2 | PV 2931 | U 51.18 | I 18.0 | conso 1893 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:09:23 | OFF_2 | PV 2719 | U 51.15 | I 14.4 | conso 1907 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:09:39 | OFF_2 | PV 2795 | U 51.13 | I 15.0 | conso 1908 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:09:54 | OFF_2 | PV 2922 | U 51.11 | I 11.7 | conso 1950 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:10:09 | OFF_2 | PV 2827 | U 51.12 | I 17.4 | conso 1895 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:10:24 | OFF_2 | PV 2770 | U 51.10 | I 13.3 | conso 1907 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:10:39 | OFF_2 | PV 2724 | U 51.09 | I 13.7 | conso 1911 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
>>> EDF ON
|
||||||
|
13:10:57 | ON_2 | PV 2760 | U 51.09 | I 13.8 | conso 1967 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:11:12 | ON_2 | PV 2796 | U 51.10 | I 13.7 | conso 1966 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:11:27 | ON_2 | PV 2959 | U 51.14 | I 18.3 | conso 1904 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:11:42 | ON_2 | PV 2734 | U 51.19 | I 16.4 | conso 1902 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:11:57 | ON_2 | PV 2663 | U 51.15 | I 12.9 | conso 1899 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:12:12 | ON_2 | PV 559 | U 51.16 | I 14.3 | conso 1886 | grid 1777 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:12:27 | ON_2 | PV 800 | U 51.07 | I 4.4 | conso 1935 | grid 1613 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:12:42 | ON_2 | PV 1112 | U 51.06 | I 9.4 | conso 1923 | grid 1361 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt MPPT MPPT MPPT volt volt
|
||||||
|
13:12:57 | ON_2 | PV 1405 | U 51.06 | I 10.5 | conso 1877 | grid 1136 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:13:12 | ON_2 | PV 1707 | U 51.05 | I 12.0 | conso 1941 | grid 852 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt MPPT MPPT volt volt
|
||||||
|
13:13:27 | ON_2 | PV 1912 | U 51.06 | I 12.1 | conso 1891 | grid 683 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:13:42 | ON_2 | PV 2071 | U 51.05 | I 8.9 | conso 2263 | grid 788 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:13:57 | ON_2 | PV 2378 | U 51.03 | I 9.2 | conso 2335 | grid 693 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:14:12 | ON_2 | PV 2885 | U 51.01 | I 7.4 | conso 2428 | grid 542 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt MPPT MPPT volt volt volt
|
||||||
|
13:14:27 | ON_2 | PV 3008 | U 51.11 | I 15.0 | conso 2362 | grid 258 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
13:14:42 | ON_2 | PV 3158 | U 51.14 | I 13.8 | conso 2373 | grid 112 | SOC 92 | DVCC 80A/51.3V | acin 1 | edf on | volt volt volt volt volt volt volt
|
||||||
|
>>> EDF OFF
|
||||||
|
13:15:01 | FIN_OFF| PV 3293 | U 51.14 | I 11.5 | conso 2270 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:15:16 | FIN_OFF| PV 3016 | U 51.17 | I 14.0 | conso 2293 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:15:31 | FIN_OFF| PV 3031 | U 51.13 | I 12.4 | conso 2268 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:15:46 | FIN_OFF| PV 3022 | U 51.14 | I 11.8 | conso 2268 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:16:01 | FIN_OFF| PV 2643 | U 51.15 | I 11.9 | conso 1900 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:16:16 | FIN_OFF| PV 2420 | U 51.13 | I 8.0 | conso 1949 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:16:31 | FIN_OFF| PV 2669 | U 51.12 | I 10.1 | conso 1976 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:16:46 | FIN_OFF| PV 2478 | U 51.12 | I 9.0 | conso 1901 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:17:01 | FIN_OFF| PV 2616 | U 51.09 | I 8.5 | conso 1973 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:17:16 | FIN_OFF| PV 2835 | U 51.10 | I 9.8 | conso 1996 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:17:31 | FIN_OFF| PV 2653 | U 51.17 | I 14.3 | conso 2004 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:17:46 | FIN_OFF| PV 2436 | U 51.15 | I 8.1 | conso 1955 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:18:01 | FIN_OFF| PV 2477 | U 51.13 | I 9.4 | conso 1965 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:18:16 | FIN_OFF| PV 2514 | U 51.13 | I 8.4 | conso 1956 | grid 0 | SOC 92 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:18:31 | FIN_OFF| PV 2514 | U 51.14 | I 8.7 | conso 1964 | grid 0 | SOC 93 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
13:18:46 | FIN_OFF| PV 2498 | U 51.13 | I 7.8 | conso 1955 | grid 0 | SOC 93 | DVCC 80A/51.3V | acin 0 | edf off | volt volt volt volt volt volt volt
|
||||||
|
>>> RESTAURATION (fin de campagne)
|
||||||
|
>>> etat restaure : edf off | latch off | hysteresis on
|
||||||
488
package-lock.json
generated
Normal file
488
package-lock.json
generated
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
{
|
||||||
|
"name": "evcs-tools",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "evcs-tools",
|
||||||
|
"dependencies": {
|
||||||
|
"mqtt": "^5.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "26.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz",
|
||||||
|
"integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~8.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/readable-stream": {
|
||||||
|
"version": "4.0.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz",
|
||||||
|
"integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "6.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
||||||
|
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/readable-stream": "^4.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/broker-factory": {
|
||||||
|
"version": "3.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.15.tgz",
|
||||||
|
"integrity": "sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.7",
|
||||||
|
"fast-unique-numbers": "^9.0.27",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-factory": "^7.0.50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||||
|
},
|
||||||
|
"node_modules/commist": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw=="
|
||||||
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/concat-stream/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-unique-numbers": {
|
||||||
|
"version": "9.0.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz",
|
||||||
|
"integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.2",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/help-me": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="
|
||||||
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz",
|
||||||
|
"integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-sdsl": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/js-sdsl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
||||||
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt": {
|
||||||
|
"version": "5.15.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.2.tgz",
|
||||||
|
"integrity": "sha512-VWZU2CSUY3U3oN0PSBRDE5SNsFi4zqqNeQ/uv3pZWqY3CrBXD/dhd0ZLjlsk5YnebGlrapi4lRVNJPUNQ5aZ3w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/readable-stream": "^4.0.21",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"commist": "^3.2.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"debug": "^4.4.1",
|
||||||
|
"help-me": "^5.0.0",
|
||||||
|
"lru-cache": "^10.4.3",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"mqtt-packet": "^9.0.2",
|
||||||
|
"number-allocator": "^1.0.14",
|
||||||
|
"readable-stream": "^4.7.0",
|
||||||
|
"rfdc": "^1.4.1",
|
||||||
|
"socks": "^2.8.6",
|
||||||
|
"split2": "^4.2.0",
|
||||||
|
"worker-timers": "^8.0.23",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mqtt": "build/bin/mqtt.js",
|
||||||
|
"mqtt_pub": "build/bin/pub.js",
|
||||||
|
"mqtt_sub": "build/bin/sub.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt-packet": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^6.0.8",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"process-nextick-args": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
|
},
|
||||||
|
"node_modules/number-allocator": {
|
||||||
|
"version": "1.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
|
||||||
|
"integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.1",
|
||||||
|
"js-sdsl": "4.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rfdc": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
|
||||||
|
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.1.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="
|
||||||
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
|
},
|
||||||
|
"node_modules/worker-factory": {
|
||||||
|
"version": "7.0.50",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.50.tgz",
|
||||||
|
"integrity": "sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.7",
|
||||||
|
"fast-unique-numbers": "^9.0.27",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers": {
|
||||||
|
"version": "8.0.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.34.tgz",
|
||||||
|
"integrity": "sha512-WXL+Dqsm0G6dnC66rQsvM3tPT2adhbqSirWUCZGglkALOr8ocS0KlpU0iuKbjflJOlu3ydZikMEVrYnHOM9suw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.7",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-timers-broker": "^8.0.18",
|
||||||
|
"worker-timers-worker": "^9.0.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers-broker": {
|
||||||
|
"version": "8.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.18.tgz",
|
||||||
|
"integrity": "sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.7",
|
||||||
|
"broker-factory": "^3.1.15",
|
||||||
|
"fast-unique-numbers": "^9.0.27",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-timers-worker": "^9.0.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers-worker": {
|
||||||
|
"version": "9.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.15.tgz",
|
||||||
|
"integrity": "sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.7",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-factory": "^7.0.50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
package.json
Normal file
9
package.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "evcs-tools",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "Outillage EVCS. Seul le chien de garde a une dependance externe (client MQTT vers le GX Venus) ; tout le reste tourne sur le Node de base.",
|
||||||
|
"dependencies": {
|
||||||
|
"mqtt": "^5.10.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -563,8 +563,17 @@ const batteryAssistSolarHoldOk =
|
|||||||
// =====================================================
|
// =====================================================
|
||||||
// START / HOLD CONDITIONS
|
// START / HOLD CONDITIONS
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
// FENETRE D'ETABLISSEMENT (31/08/2026) — cf. WINDOWS_SOURCEV2 pour la mesure.
|
||||||
|
// Pendant les ~150 s qui suivent l'arrivee du reseau, PV et courant batterie ne
|
||||||
|
// mesurent rien d'exploitable. On NE DEMARRE PAS sur ces valeurs, et on ne
|
||||||
|
// laisse pas une charge en cours s'arreter a cause d'elles. Les gardes de
|
||||||
|
// securite (commonSafety*, planchers SOC, surcharge, pas de tampon) restent
|
||||||
|
// evaluees normalement : c'est le risque qui prime, jamais le confort.
|
||||||
|
const gridSettling = derived.gridSettling === true;
|
||||||
|
|
||||||
const rawSolarStartOk =
|
const rawSolarStartOk =
|
||||||
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||||
|
!gridSettling &&
|
||||||
commonSafetyStartOk &&
|
commonSafetyStartOk &&
|
||||||
parsed.soc >= effectiveSocStartFloor &&
|
parsed.soc >= effectiveSocStartFloor &&
|
||||||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
@@ -580,8 +589,12 @@ const rawSolarHoldOk =
|
|||||||
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||||
commonSafetyHoldOk &&
|
commonSafetyHoldOk &&
|
||||||
parsed.soc >= effectiveSocHoldFloor &&
|
parsed.soc >= effectiveSocHoldFloor &&
|
||||||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
|
||||||
(
|
(
|
||||||
|
gridSettling ||
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA
|
||||||
|
) &&
|
||||||
|
(
|
||||||
|
gridSettling ||
|
||||||
parsed.pvPowerW >= params.pvHoldMinW ||
|
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||||
cloudToleranceOk
|
cloudToleranceOk
|
||||||
);
|
);
|
||||||
@@ -667,9 +680,13 @@ const probeSuccessNow =
|
|||||||
parsed.batteryCurrentA >= Math.min(0, batteryDischargeSoftDynamicA)
|
parsed.batteryCurrentA >= Math.min(0, batteryDischargeSoftDynamicA)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Une sonde ne peut pas etre declaree ratee sur une mesure prise pendant
|
||||||
|
// l'etablissement : c'est le reseau qui masque la production, pas la sonde qui
|
||||||
|
// echoue. On differe le verdict, on ne l'invente pas.
|
||||||
const probeFailedNow =
|
const probeFailedNow =
|
||||||
probeModeActive &&
|
probeModeActive &&
|
||||||
probeElapsedReached &&
|
probeElapsedReached &&
|
||||||
|
!gridSettling &&
|
||||||
!probeSuccessNow;
|
!probeSuccessNow;
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -689,7 +706,10 @@ const stepUpByForecastMode =
|
|||||||
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 5) &&
|
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 5) &&
|
||||||
cloudToleranceOk;
|
cloudToleranceOk;
|
||||||
|
|
||||||
|
// Pas de montee en courant sur des mesures d'etablissement. La descente, elle,
|
||||||
|
// n'est pas bridee : c'est la direction sure.
|
||||||
const solarShouldStepUp =
|
const solarShouldStepUp =
|
||||||
|
!gridSettling &&
|
||||||
lifecycle.currentCooldownElapsed &&
|
lifecycle.currentCooldownElapsed &&
|
||||||
derived.currentReferenceA < derived.physicalCapA &&
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
derived.physicalCapA >= params.currentMinA &&
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
@@ -906,6 +926,8 @@ return [{
|
|||||||
|
|
||||||
solarStartOk,
|
solarStartOk,
|
||||||
solarHoldOk,
|
solarHoldOk,
|
||||||
|
gridSettling,
|
||||||
|
gridTransitionAgeSec: parsed.gridTransitionAgeSec,
|
||||||
gridStartOk,
|
gridStartOk,
|
||||||
gridHoldOk,
|
gridHoldOk,
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,17 @@ pushAction(
|
|||||||
context.sourceMode === 'grid_hc_allowed' || context.sourceMode === 'grid_forced'
|
context.sourceMode === 'grid_hc_allowed' || context.sourceMode === 'grid_forced'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// CONSIGNE DE PUISSANCE RESEAU
|
||||||
|
// Fermer le contacteur ne suffit pas : sans consigne, l'ESS laisse la
|
||||||
|
// batterie porter la maison et la voiture (nuit du 01/09/2026).
|
||||||
|
// =====================================================
|
||||||
|
pushAction(
|
||||||
|
'grid_power_setpoint',
|
||||||
|
calls.grid_power_setpoint,
|
||||||
|
!!calls.grid_power_setpoint?.execute
|
||||||
|
);
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// AUTO START OFF SI BESOIN
|
// AUTO START OFF SI BESOIN
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|||||||
@@ -834,18 +834,20 @@ else if (cloud.sessionObservedActive) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if ((derived.currentReferenceA > derived.physicalCapA) && lifecycle.currentCooldownElapsed) {
|
// SÉQUENCE D'OUVERTURE (31/08/2026) : un plafond n'est pas une cible qu'on
|
||||||
|
// approche, c'est une limite qu'on respecte. Cette réduction attendait le
|
||||||
|
// cooldown de courant — un compteur de confort — et ne descendait que d'un
|
||||||
|
// pas à la fois : de 28 A à 20 A elle prenait plusieurs cooldowns, pendant
|
||||||
|
// lesquels les onduleurs portaient un courant qu'ils n'ont plus le droit de
|
||||||
|
// porter (le plafond retombe à la disparition du réseau). Désormais :
|
||||||
|
// descente IMMÉDIATE et DIRECTE au plafond. Réduire est toujours sûr ; c'est
|
||||||
|
// la montée qui doit être temporisée, et elle l'est.
|
||||||
|
else if (derived.currentReferenceA > derived.physicalCapA) {
|
||||||
setOn({
|
setOn({
|
||||||
stateName: 'charging_step_down_system_cap',
|
stateName: 'charging_step_down_system_cap',
|
||||||
decisionName: 'step_down',
|
decisionName: 'step_down_fast',
|
||||||
currentA: Math.max(
|
currentA: Math.max(params.currentMinA, derived.physicalCapA),
|
||||||
params.currentMinA,
|
reasonText: `Réduction immédiate au plafond système ${derived.physicalCapA}A`,
|
||||||
Math.min(
|
|
||||||
derived.currentReferenceA - Math.max(params.currentStepDownA, 2),
|
|
||||||
derived.physicalCapA
|
|
||||||
)
|
|
||||||
),
|
|
||||||
reasonText: 'Réduction pour respecter la capacité système',
|
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
mustStopFlag: false,
|
mustStopFlag: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
// Bornes de la consigne de puissance reseau. Le maximum est celui declare par
|
||||||
|
// l'entite elle-meme (number.airtria_mpii_v1_ac_power_setpoint_l1, max 10000 W),
|
||||||
|
// pas un chiffre choisi ici. La bande morte evite de reecrire la meme valeur
|
||||||
|
// toutes les 30 s sans empecher la correction d'une derive externe.
|
||||||
|
const GRID_SETPOINT_MAX_W = 10000;
|
||||||
|
const GRID_SETPOINT_DEADBAND_W = 100;
|
||||||
|
|
||||||
function round1(v) {
|
function round1(v) {
|
||||||
return Math.round(v * 10) / 10;
|
return Math.round(v * 10) / 10;
|
||||||
}
|
}
|
||||||
@@ -309,7 +316,77 @@ reason.push(`SOC=${round1(parsed.soc)}%`);
|
|||||||
reason.push(`Batt=${round1(parsed.batteryCurrentA)}A`);
|
reason.push(`Batt=${round1(parsed.batteryCurrentA)}A`);
|
||||||
reason.push(`PV=${Math.round(parsed.pvPowerW)}W`);
|
reason.push(`PV=${Math.round(parsed.pvPowerW)}W`);
|
||||||
reason.push(`PV10=${Math.round(parsed.pvPowerAvg10mW)}W`);
|
reason.push(`PV10=${Math.round(parsed.pvPowerAvg10mW)}W`);
|
||||||
|
// =====================================================
|
||||||
|
// CONSIGNE DE PUISSANCE RESEAU (01/09/2026)
|
||||||
|
// =====================================================
|
||||||
|
// Fermer le contacteur ne suffit PAS. Au-dessus du SOC minimal, l'ESS en
|
||||||
|
// autoconsommation ne preleve que `ac_power_setpoint` et laisse la batterie
|
||||||
|
// porter tout le reste : nuit du 01/09, contacteur ferme, consigne 50 W,
|
||||||
|
// batterie a -803 W pour 761 W de maison, voiture arretee par le garde
|
||||||
|
// "batterie trop sollicitee". La consigne est donc le levier qui rend une
|
||||||
|
// session reseau REELLE, et il doit appartenir au controleur, pas rester un
|
||||||
|
// reglage manuel permanent (celui-ci provoquait la substitution du solaire
|
||||||
|
// par le reseau en journee, cf. HANDOFF 27/28).
|
||||||
|
//
|
||||||
|
// La valeur n'est pas une constante : c'est le besoin mesure du moment, la
|
||||||
|
// maison telle qu'elle consomme plus la voiture au courant que le controleur
|
||||||
|
// s'autorise. Le plafond joue le role de marge : quand la voiture n'est pas a
|
||||||
|
// son plafond, la consigne est deja genereuse, et l'exces ne peut pas partir
|
||||||
|
// dans la batterie (DVCC tenue a 2 A par l'Auto Charge Manager pendant un
|
||||||
|
// takeover). Hors session, retour a la valeur de repos.
|
||||||
|
const gridSessionActive =
|
||||||
|
parsed.acInAvailable &&
|
||||||
|
(context.sourceMode === 'grid_forced' || context.sourceMode === 'grid_hc_allowed');
|
||||||
|
|
||||||
|
const gridSetpointNeedW = gridSessionActive
|
||||||
|
? clamp(
|
||||||
|
Math.round(derived.houseNonEvcsW + derived.physicalCapA * derived.acVoltageRef),
|
||||||
|
params.gridSetpointIdleW,
|
||||||
|
GRID_SETPOINT_MAX_W
|
||||||
|
)
|
||||||
|
: params.gridSetpointIdleW;
|
||||||
|
|
||||||
|
// La bande est ASYMETRIQUE, et c'est le point. Une consigne trop BASSE fait
|
||||||
|
// porter la difference par la batterie : c'est le defaut de la nuit du 01/09,
|
||||||
|
// il se corrige immediatement. Une consigne trop HAUTE ne coute rien : l'ESS
|
||||||
|
// ne preleve que ce qu'il peut placer, et le courant de charge de la batterie
|
||||||
|
// est tenu par ailleurs (DVCC, Auto Charge Manager). Deux leviers, deux roles
|
||||||
|
// -- la consigne dit ce que le reseau a le DROIT de servir, DVCC dit ce que la
|
||||||
|
// batterie a le droit de prendre. Ne pas confondre les deux.
|
||||||
|
//
|
||||||
|
// On ecrit donc avec une marge d'un pas de courant EVCS (grandeur du systeme,
|
||||||
|
// pas une constante inventee) pour que le bruit de la maison ne rende pas la
|
||||||
|
// consigne insuffisante au cycle suivant, et on ne redescend que si elle est
|
||||||
|
// devenue franchement inutile. Sans cela la boucle 30 s reecrivait a chaque
|
||||||
|
// cycle en poursuivant le bruit.
|
||||||
|
const gridSetpointSlackW = gridSessionActive
|
||||||
|
? Math.max(GRID_SETPOINT_DEADBAND_W, Math.round(params.currentStepUpA * derived.acVoltageRef))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridSetpointTargetW = clamp(
|
||||||
|
gridSetpointNeedW + gridSetpointSlackW,
|
||||||
|
0,
|
||||||
|
GRID_SETPOINT_MAX_W
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridSetpointCurrentW = Number.isFinite(parsed.gridSetpointW)
|
||||||
|
? parsed.gridSetpointW
|
||||||
|
: null;
|
||||||
|
const gridSetpointNeedsWrite =
|
||||||
|
gridSetpointCurrentW === null ||
|
||||||
|
gridSetpointCurrentW < gridSetpointNeedW ||
|
||||||
|
gridSetpointCurrentW > gridSetpointNeedW + 2 * gridSetpointSlackW + GRID_SETPOINT_DEADBAND_W;
|
||||||
|
|
||||||
reason.push(`Grid=${parsed.gridAvailable ? 'on' : 'off'} (${Math.round(parsed.gridPowerW)}W)`);
|
reason.push(`Grid=${parsed.gridAvailable ? 'on' : 'off'} (${Math.round(parsed.gridPowerW)}W)`);
|
||||||
|
// Trace de la fenetre d'etablissement : sans elle, une lecture du logbook
|
||||||
|
// pendant le transitoire donne l'impression que le systeme decide n'importe
|
||||||
|
// quoi. Il decide, au contraire, de ne PAS decider sur ces mesures.
|
||||||
|
if (derived.gridSettling) {
|
||||||
|
reason.push(`ETABLISSEMENT reseau ${Math.round(parsed.gridTransitionAgeSec)}s/${params.gridSettleSeconds}s (mesures PV/batterie non fiables)`);
|
||||||
|
}
|
||||||
|
if (gridSessionActive) {
|
||||||
|
reason.push(`Consigne reseau=${gridSetpointTargetW}W`);
|
||||||
|
}
|
||||||
reason.push(`Cap=${derived.physicalCapA}A`);
|
reason.push(`Cap=${derived.physicalCapA}A`);
|
||||||
reason.push(`Ref=${round1(derived.currentReferenceA)}A`);
|
reason.push(`Ref=${round1(derived.currentReferenceA)}A`);
|
||||||
reason.push(`Target=${round1(desiredCurrentA)}A`);
|
reason.push(`Target=${round1(desiredCurrentA)}A`);
|
||||||
@@ -995,6 +1072,16 @@ return [{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
grid_power_setpoint: {
|
||||||
|
execute: gridSetpointNeedsWrite,
|
||||||
|
domain: 'number',
|
||||||
|
service: 'set_value',
|
||||||
|
data: {
|
||||||
|
entity_id: 'number.gx_device_ac_power_setpoint',
|
||||||
|
value: gridSetpointTargetW
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
evcs_auto_start_off: {
|
evcs_auto_start_off: {
|
||||||
execute: autoStartAction === 'turn_off',
|
execute: autoStartAction === 'turn_off',
|
||||||
domain: 'switch',
|
domain: 'switch',
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
"grid_state": states("switch.edf_general_ok_switch"),
|
"grid_state": states("switch.edf_general_ok_switch"),
|
||||||
"victron_acin1_available": states("sensor.victron_ac_in1_available"),
|
"victron_acin1_available": states("sensor.victron_ac_in1_available"),
|
||||||
|
|
||||||
|
|
||||||
|
"acin1_last_changed_ts": as_timestamp(states.sensor.victron_ac_in1_available.last_changed, 0) if states.sensor.victron_ac_in1_available is not none else 0,
|
||||||
|
"grid_switch_last_changed_ts": as_timestamp(states.switch.edf_general_ok_switch.last_changed, 0) if states.switch.edf_general_ok_switch is not none else 0,
|
||||||
|
"grid_settle_seconds": states("input_number.evcs_grid_settle_seconds"),
|
||||||
|
"grid_setpoint_w": states("number.gx_device_ac_power_setpoint"),
|
||||||
|
"grid_setpoint_idle_w": states("input_number.evcs_grid_setpoint_idle_w"),
|
||||||
|
|
||||||
"today_remaining_kwh": states("sensor.energy_production_today_remaining"),
|
"today_remaining_kwh": states("sensor.energy_production_today_remaining"),
|
||||||
"tomorrow_kwh": states("sensor.energy_production_tomorrow"),
|
"tomorrow_kwh": states("sensor.energy_production_tomorrow"),
|
||||||
|
|
||||||
|
|||||||
@@ -245,10 +245,32 @@ const parsed = {
|
|||||||
|
|
||||||
gridIsOn: low(input.grid_state || 'off') === 'on',
|
gridIsOn: low(input.grid_state || 'off') === 'on',
|
||||||
acInAvailable: num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1,
|
acInAvailable: num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1,
|
||||||
|
|
||||||
|
// Consigne de puissance réseau de l'ESS, lue telle qu'elle est appliquée.
|
||||||
|
// C'est ELLE qui décide si un contacteur fermé sert à quelque chose : au-dessus
|
||||||
|
// du SOC minimal, l'ESS en autoconsommation ne prélève que cette puissance et
|
||||||
|
// laisse la batterie porter le reste. Nuit du 01/09 : contacteur fermé,
|
||||||
|
// consigne 50 W, batterie à -803 W pour 761 W de maison.
|
||||||
|
gridSetpointW: num(input.grid_setpoint_w, NaN),
|
||||||
gridAvailable:
|
gridAvailable:
|
||||||
(low(input.grid_state || 'off') === 'on') ||
|
(low(input.grid_state || 'off') === 'on') ||
|
||||||
(num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1),
|
(num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1),
|
||||||
|
|
||||||
|
// FENETRE D'ETABLISSEMENT (31/08/2026) — age, en secondes, de la derniere
|
||||||
|
// transition du reseau. On retient la plus RECENTE des deux sources : la
|
||||||
|
// presence physique AC-IN (celle qui declenche reellement le transitoire) et
|
||||||
|
// le contacteur (qui peut bouger sans qu'AC-IN suive, disjoncteur ouvert).
|
||||||
|
// Horodatage absent ou nul => age tres grand => aucune fenetre, comportement
|
||||||
|
// historique. On ne devine jamais une transition qu'on n'a pas observee.
|
||||||
|
gridTransitionAgeSec: (() => {
|
||||||
|
const ts = Math.max(
|
||||||
|
num(input.acin1_last_changed_ts, 0),
|
||||||
|
num(input.grid_switch_last_changed_ts, 0)
|
||||||
|
);
|
||||||
|
if (!(ts > 0)) return Number.POSITIVE_INFINITY;
|
||||||
|
return Math.max(0, (nowMs / 1000) - ts);
|
||||||
|
})(),
|
||||||
|
|
||||||
todayRemainingKwh: Math.max(0, num(input.today_remaining_kwh, 0)),
|
todayRemainingKwh: Math.max(0, num(input.today_remaining_kwh, 0)),
|
||||||
tomorrowKwh: Math.max(0, num(input.tomorrow_kwh, 0)),
|
tomorrowKwh: Math.max(0, num(input.tomorrow_kwh, 0)),
|
||||||
|
|
||||||
@@ -398,6 +420,16 @@ const params = {
|
|||||||
advisorStatusOk: cleanText(input.advisor_status).toLowerCase() === 'ok',
|
advisorStatusOk: cleanText(input.advisor_status).toLowerCase() === 'ok',
|
||||||
advisorLastRunMs: parseAdvisorRunToMs(cleanText(input.advisor_last_run)),
|
advisorLastRunMs: parseAdvisorRunToMs(cleanText(input.advisor_last_run)),
|
||||||
|
|
||||||
|
// Duree de la fenetre d'etablissement. Valeur mesuree le 31/08/2026 : le
|
||||||
|
// transitoire de fermeture dure ~150 s (import 1985 -> 25 W et 2600 -> 52 W,
|
||||||
|
// deux campagnes), on prend 180 s de marge. Reglable sans redeploiement via
|
||||||
|
// input_number.evcs_grid_settle_seconds ; absent => 180.
|
||||||
|
gridSettleSeconds: clamp(num(input.grid_settle_seconds, 180), 0, 900),
|
||||||
|
|
||||||
|
// Consigne réseau au repos. 50 W est le défaut Victron (légèrement positif
|
||||||
|
// pour ne jamais injecter), pas une valeur choisie ici.
|
||||||
|
gridSetpointIdleW: clamp(num(input.grid_setpoint_idle_w, 50), 0, 500),
|
||||||
|
|
||||||
probeEnabled: low(input.probe_enabled || 'off') === 'on',
|
probeEnabled: low(input.probe_enabled || 'off') === 'on',
|
||||||
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
|
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
|
||||||
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),
|
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),
|
||||||
|
|||||||
@@ -117,24 +117,27 @@ const vebusNonEvcsW = Math.max(0, parsed.vebusAcOutPowerW - parsed.evcsAcPowerW)
|
|||||||
const totalVebusSoftCeilingW = Math.max(6500, params.acLoadsMaxSafeW || 8500);
|
const totalVebusSoftCeilingW = Math.max(6500, params.acLoadsMaxSafeW || 8500);
|
||||||
const totalVebusHardCeilingW = totalVebusSoftCeilingW + 400;
|
const totalVebusHardCeilingW = totalVebusSoftCeilingW + 400;
|
||||||
|
|
||||||
// P5 : plafond EVCS configurable quand le réseau alimente réellement AC-IN
|
// =====================================================
|
||||||
// (input_number.evcs_plafond_courant_edf_on). Hors réseau : plafond câblé prudent.
|
// FENETRE D'ETABLISSEMENT DU RESEAU (31/08/2026)
|
||||||
const evcsDedicatedCapBaseW = Math.max(0, cfg.evcsDedicatedMaxW - cfg.systemReserveW);
|
// =====================================================
|
||||||
const safeEvcsByDedicatedCapW = parsed.acInAvailable
|
// Mesure du 31/08 : a la fermeture du contacteur, le reseau reprend d'un coup
|
||||||
? Math.max(0, params.gridCurrentMaxA * acVoltageRef)
|
// TOUTE la charge de la maison et le solaire bascule vers la batterie. Batterie
|
||||||
: evcsDedicatedCapBaseW;
|
// preneuse, la production ne bouge pas ; batterie pleine, elle s'effondre (2663
|
||||||
const safeEvcsByVebusW = Math.max(0, totalVebusSoftCeilingW - vebusNonEvcsW - cfg.systemReserveW);
|
// -> 559 W) faute de destination. Dans les deux cas l'import decroit ensuite
|
||||||
const safeEvcsCombinedW = Math.max(0, Math.min(safeEvcsByDedicatedCapW, safeEvcsByVebusW));
|
// jusqu'a la consigne en ~150 s (1985 -> 25 W, puis 2600 -> 52 W).
|
||||||
|
//
|
||||||
const systemCapCurrentA = Math.max(0, Math.floor(safeEvcsCombinedW / acVoltageRef));
|
// Pendant cette phase, PV, import et courant batterie ne decrivent aucun regime
|
||||||
const physicalCapA = clamp(
|
// permanent : la boucle de 30 s y prend cinq decisions, et la moyenne PV 10 min
|
||||||
systemCapCurrentA,
|
// en garde la trace une dizaine de minutes de plus. Le systeme lit "plus de
|
||||||
0,
|
// soleil", en conclut qu'il a besoin du reseau, et confirme ainsi sa propre
|
||||||
parsed.acInAvailable
|
// premisse -- une boucle de rattrapage, comme les trois du 24/08.
|
||||||
? Math.max(params.currentMaxAutoA, params.gridCurrentMaxA)
|
//
|
||||||
: params.currentMaxAutoA
|
// ASYMETRIE VOULUE : la fenetre ne s'ouvre qu'a l'ARRIVEE du reseau. A sa
|
||||||
);
|
// disparition, les onduleurs reprennent instantanement toute la charge : les
|
||||||
const canRunAtMinCurrent = physicalCapA >= params.currentMinA;
|
// gardes doivent agir plus vite, jamais moins vite.
|
||||||
|
const gridSettling =
|
||||||
|
parsed.gridAvailable &&
|
||||||
|
parsed.gridTransitionAgeSec < params.gridSettleSeconds;
|
||||||
|
|
||||||
const endDayGuardSoc = Math.max(params.endOfDaySocGuard, params.endOfDaySocTarget);
|
const endDayGuardSoc = Math.max(params.endOfDaySocGuard, params.endOfDaySocTarget);
|
||||||
|
|
||||||
@@ -757,6 +760,50 @@ if (dayVehicleFullIdle && profile !== 'night_hc') {
|
|||||||
sourceMode = 'blocked';
|
sourceMode = 'blocked';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// PLAFOND DE COURANT EVCS — calculé APRÈS sourceMode (31/08/2026)
|
||||||
|
// =====================================================
|
||||||
|
// P5 : plafond majoré (input_number.evcs_plafond_courant_edf_on, jusqu'à 28 A
|
||||||
|
// en BOOST) quand le réseau alimente réellement AC-IN ; sinon plafond câblé
|
||||||
|
// prudent, celui que les deux MultiPlus peuvent porter SEULS.
|
||||||
|
//
|
||||||
|
// SÉQUENCE D'OUVERTURE (31/08/2026) — pourquoi ce bloc a été déplacé ici.
|
||||||
|
// Le plafond majoré n'est légitime que tant que le réseau est là. Or c'est
|
||||||
|
// l'hystérésis (cycle 1 min) qui ouvre le contacteur, et le contrôleur EVCS ne
|
||||||
|
// l'apprenait qu'après coup : pendant ce temps les deux onduleurs portaient
|
||||||
|
// seuls la voiture au courant majoré plus la maison — le mécanisme du W-105.
|
||||||
|
//
|
||||||
|
// Correctif sans poignée de main entre les deux automates : le plafond majoré
|
||||||
|
// exige désormais que l'EVCS DEMANDE lui-même le réseau. Sa demande tombe au
|
||||||
|
// cycle où la fenêtre se ferme (30 s), alors que l'hystérésis ne peut ouvrir
|
||||||
|
// qu'ensuite (override disparu ET min_on écoulé, cycle 1 min). L'EVCS baisse
|
||||||
|
// donc AVANT, mécaniquement, sans rien coordonner.
|
||||||
|
// Le calcul doit venir après sourceMode ; rien entre les deux ne l'utilisait.
|
||||||
|
const evcsGridCeilingAllowed =
|
||||||
|
parsed.acInAvailable &&
|
||||||
|
(
|
||||||
|
sourceMode === 'grid_forced' ||
|
||||||
|
sourceMode === 'grid_hc_allowed' ||
|
||||||
|
forceGridRequested
|
||||||
|
);
|
||||||
|
|
||||||
|
const evcsDedicatedCapBaseW = Math.max(0, cfg.evcsDedicatedMaxW - cfg.systemReserveW);
|
||||||
|
const safeEvcsByDedicatedCapW = evcsGridCeilingAllowed
|
||||||
|
? Math.max(0, params.gridCurrentMaxA * acVoltageRef)
|
||||||
|
: evcsDedicatedCapBaseW;
|
||||||
|
const safeEvcsByVebusW = Math.max(0, totalVebusSoftCeilingW - vebusNonEvcsW - cfg.systemReserveW);
|
||||||
|
const safeEvcsCombinedW = Math.max(0, Math.min(safeEvcsByDedicatedCapW, safeEvcsByVebusW));
|
||||||
|
|
||||||
|
const systemCapCurrentA = Math.max(0, Math.floor(safeEvcsCombinedW / acVoltageRef));
|
||||||
|
const physicalCapA = clamp(
|
||||||
|
systemCapCurrentA,
|
||||||
|
0,
|
||||||
|
evcsGridCeilingAllowed
|
||||||
|
? Math.max(params.currentMaxAutoA, params.gridCurrentMaxA)
|
||||||
|
: params.currentMaxAutoA
|
||||||
|
);
|
||||||
|
const canRunAtMinCurrent = physicalCapA >= params.currentMinA;
|
||||||
|
|
||||||
const operatingMode = profile;
|
const operatingMode = profile;
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -805,6 +852,9 @@ return [{
|
|||||||
systemCapCurrentA,
|
systemCapCurrentA,
|
||||||
physicalCapA,
|
physicalCapA,
|
||||||
canRunAtMinCurrent,
|
canRunAtMinCurrent,
|
||||||
|
evcsGridCeilingAllowed,
|
||||||
|
gridSettling,
|
||||||
|
gridTransitionAgeSec: parsed.gridTransitionAgeSec,
|
||||||
endDayGuardSoc,
|
endDayGuardSoc,
|
||||||
energyNeededForEndDayGuardKwh,
|
energyNeededForEndDayGuardKwh,
|
||||||
forecastBufferKwh,
|
forecastBufferKwh,
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ function summarize(out) {
|
|||||||
takeoverNeeded: !!c.night_grid_takeover_needed,
|
takeoverNeeded: !!c.night_grid_takeover_needed,
|
||||||
edfOnAction: !!calls.edf_general_ok_on?.execute,
|
edfOnAction: !!calls.edf_general_ok_on?.execute,
|
||||||
takeoverHelperService: calls.helper_night_grid_takeover_needed?.service || null,
|
takeoverHelperService: calls.helper_night_grid_takeover_needed?.service || null,
|
||||||
|
// Consigne de puissance reseau ecrite ce cycle (null = aucune ecriture).
|
||||||
|
gridSetpointW: calls.grid_power_setpoint?.execute
|
||||||
|
? calls.grid_power_setpoint.data.value
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ const MIDDAY = '2026-08-08T13:00:00+02:00';
|
|||||||
const AFTERNOON = '2026-08-08T15:30:00+02:00';
|
const AFTERNOON = '2026-08-08T15:30:00+02:00';
|
||||||
const EVENING = '2026-08-08T18:00:00+02:00';
|
const EVENING = '2026-08-08T18:00:00+02:00';
|
||||||
|
|
||||||
|
// Fenêtre d'établissement du réseau (31/08/2026) : horodatage de la dernière
|
||||||
|
// transition d'AC-IN, en secondes epoch. Le harnais fige l'horloge sur `time`,
|
||||||
|
// donc on exprime l'âge de la transition par rapport à cet instant simulé.
|
||||||
|
const MORNING_TS = Date.parse(MORNING) / 1000;
|
||||||
|
|
||||||
const VEHICLE_NIGHT = {
|
const VEHICLE_NIGHT = {
|
||||||
evcs_vehicle_connected: 'on',
|
evcs_vehicle_connected: 'on',
|
||||||
evcs_device_connected: 'on',
|
evcs_device_connected: 'on',
|
||||||
@@ -1019,4 +1024,374 @@ export const scenarios = [
|
|||||||
takeoverHelperService: 'turn_off',
|
takeoverHelperService: 'turn_off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// ======= FENÊTRE D'ÉTABLISSEMENT DU RÉSEAU (31/08/2026) =======
|
||||||
|
// Mesuré ce jour-là : à la fermeture du contacteur, le réseau reprend toute la
|
||||||
|
// charge de la maison, le solaire bascule vers la batterie (ou se fait brider
|
||||||
|
// s'il n'y a plus de tampon : 2663 -> 559 W), puis l'import redescend à la
|
||||||
|
// consigne en ~150 s. Pendant ce temps la boucle de 30 s prend cinq décisions
|
||||||
|
// sur des mesures qui ne décrivent aucun régime permanent.
|
||||||
|
// Chaque cas est apparié à son contrefactuel hors fenêtre : sans cet appariement
|
||||||
|
// le test ne prouverait rien.
|
||||||
|
{
|
||||||
|
id: 'etablissement-maintien-charge',
|
||||||
|
label: 'Réseau fermé il y a 30 s, PV effondrée à 600 W et batterie à -15 A : la charge en cours NE DOIT PAS être arrêtée',
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
soc_percent: '60',
|
||||||
|
evcs_current_a: '16', evcs_set_current_a: '16', evcs_command_current_a: '16',
|
||||||
|
evcs_ac_power_w: '3680',
|
||||||
|
house_ac_power_w: '4400', vebus_ac_out_l1_power_w: '4400',
|
||||||
|
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
|
||||||
|
battery_current_a: '-15',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 30),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 30),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "charging_step_down_battery",
|
||||||
|
decision: "step_down",
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 14,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'etablissement-termine-arret-normal',
|
||||||
|
label: 'CONTREFACTUEL : mêmes mesures, transition vieille de 400 s → fenêtre fermée, la garde de maintien reprend la main et arrête',
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
soc_percent: '60',
|
||||||
|
evcs_current_a: '16', evcs_set_current_a: '16', evcs_command_current_a: '16',
|
||||||
|
evcs_ac_power_w: '3680',
|
||||||
|
house_ac_power_w: '4400', vebus_ac_out_l1_power_w: '4400',
|
||||||
|
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
|
||||||
|
battery_current_a: '-15',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 400),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 400),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "stop_conditions_lost",
|
||||||
|
decision: "stop",
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'etablissement-securite-non-suspendue',
|
||||||
|
label: "SÉCURITÉ : dans la fenêtre, batterie à -40 A → l'arrêt dur reste actif. La fenêtre suspend le confort, jamais le risque",
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
soc_percent: '60',
|
||||||
|
evcs_current_a: '16', evcs_set_current_a: '16', evcs_command_current_a: '16',
|
||||||
|
evcs_ac_power_w: '3680',
|
||||||
|
house_ac_power_w: '4400', vebus_ac_out_l1_power_w: '4400',
|
||||||
|
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
|
||||||
|
battery_current_a: '-40',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 30),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 30),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "battery_current_hard_stop",
|
||||||
|
decision: "stop_hard",
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'etablissement-ouverture-aucune-fenetre',
|
||||||
|
label: "ASYMÉTRIE : réseau DISPARU il y a 30 s → aucune fenêtre. Les onduleurs reprennent tout, les gardes doivent agir plus vite, pas moins",
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
soc_percent: '60',
|
||||||
|
evcs_current_a: '16', evcs_set_current_a: '16', evcs_command_current_a: '16',
|
||||||
|
evcs_ac_power_w: '3680',
|
||||||
|
house_ac_power_w: '4400', vebus_ac_out_l1_power_w: '4400',
|
||||||
|
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
|
||||||
|
battery_current_a: '-15',
|
||||||
|
grid_state: 'off', victron_acin1_available: '0', grid_power_w: '0',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 30),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 30),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "stop_conditions_lost",
|
||||||
|
decision: "stop",
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'etablissement-pas-de-demarrage',
|
||||||
|
label: "Réseau fermé il y a 30 s, rien en charge : on ne démarre pas non plus sur des mesures d'établissement",
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
|
||||||
|
pv_trend_w: '100', battery_current_a: '10',
|
||||||
|
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 30),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 30),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "idle_wait_conditions",
|
||||||
|
decision: "hold",
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'etablissement-demarrage-hors-fenetre',
|
||||||
|
label: 'CONTREFACTUEL : mêmes mesures, transition vieille de 400 s → le démarrage solaire a bien lieu',
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
|
||||||
|
pv_trend_w: '100', battery_current_a: '10',
|
||||||
|
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 400),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 400),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "start_min_current",
|
||||||
|
decision: "start",
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ======= SÉQUENCE D'OUVERTURE DU CONTACTEUR (31/08/2026) =======
|
||||||
|
// Le plafond majoré (28 A en BOOST) n'est légitime que tant que le réseau est
|
||||||
|
// là. C'est l'hystérésis qui ouvre, une minute après que la demande réseau de
|
||||||
|
// l'EVCS a disparu : l'EVCS doit donc baisser AVANT, de lui-même.
|
||||||
|
{
|
||||||
|
id: 'ouverture-plafond-tombe-hors-demande-reseau',
|
||||||
|
label: "Matin solaire, AC-IN encore présent mais l'EVCS ne demande plus le réseau : le plafond retombe à 20 A et la voiture est réduite IMMÉDIATEMENT",
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
// BOOST EDF : plafond réseau 28 A, voiture effectivement à 28 A.
|
||||||
|
grid_current_max_a: '28', current_max_auto_a: '22',
|
||||||
|
evcs_current_a: '28', evcs_set_current_a: '28', evcs_command_current_a: '28',
|
||||||
|
evcs_ac_power_w: '6440',
|
||||||
|
house_ac_power_w: '8440', vebus_ac_out_l1_power_w: '8440',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '2000',
|
||||||
|
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
|
||||||
|
battery_current_a: '10',
|
||||||
|
// Cooldown de courant NON écoulé : la réduction ne doit pas l'attendre.
|
||||||
|
last_current_change_at: '2026-08-08 08:59:50',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 4000),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 4000),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "charging_step_down_system_cap",
|
||||||
|
decision: "step_down_fast",
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 20,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ouverture-plafond-maintenu-en-demande-reseau',
|
||||||
|
label: "CONTREFACTUEL : force_grid_now, l'EVCS demande le réseau → le plafond BOOST 28 A est conservé, aucune réduction",
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
// BOOST EDF : plafond réseau 28 A, voiture effectivement à 28 A.
|
||||||
|
grid_current_max_a: '28', current_max_auto_a: '22',
|
||||||
|
evcs_current_a: '28', evcs_set_current_a: '28', evcs_command_current_a: '28',
|
||||||
|
evcs_ac_power_w: '6440',
|
||||||
|
house_ac_power_w: '8440', vebus_ac_out_l1_power_w: '8440',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '2000',
|
||||||
|
victron_evcs_strategy: 'force_grid_now',
|
||||||
|
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
|
||||||
|
battery_current_a: '10',
|
||||||
|
last_current_change_at: '2026-08-08 08:59:50',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 4000),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 4000),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "grid_forced",
|
||||||
|
state: "charging_hold_grid",
|
||||||
|
decision: "hold",
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 28,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ouverture-reseau-disparu-reduction-immediate',
|
||||||
|
label: 'Réseau DISPARU : plafond câblé 20 A, voiture encore à 28 A, cooldown non écoulé → réduction immédiate, sans attendre le compteur de confort',
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
// BOOST EDF : plafond réseau 28 A, voiture effectivement à 28 A.
|
||||||
|
grid_current_max_a: '28', current_max_auto_a: '22',
|
||||||
|
evcs_current_a: '28', evcs_set_current_a: '28', evcs_command_current_a: '28',
|
||||||
|
evcs_ac_power_w: '6440',
|
||||||
|
house_ac_power_w: '8440', vebus_ac_out_l1_power_w: '8440',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '2000',
|
||||||
|
grid_state: 'off', victron_acin1_available: '0', grid_power_w: '0',
|
||||||
|
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
|
||||||
|
battery_current_a: '10',
|
||||||
|
last_current_change_at: '2026-08-08 08:59:50',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 20),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 20),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "charging_step_down_system_cap",
|
||||||
|
decision: "step_down_fast",
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 20,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// CONSIGNE DE PUISSANCE RESEAU (01/09/2026)
|
||||||
|
// Fermer le contacteur ne suffit pas : au-dessus du SOC minimal, l'ESS en
|
||||||
|
// autoconsommation ne preleve que `ac_power_setpoint` et laisse la batterie
|
||||||
|
// porter maison + voiture. Incident de la nuit du 01/09 : consigne a 50 W,
|
||||||
|
// charge HC demarree puis arretee en 60 s par le garde batterie.
|
||||||
|
// ===================================================================
|
||||||
|
{
|
||||||
|
id: 'consigne-session-hc-reseau-porte-la-charge',
|
||||||
|
label: 'Nuit HC, reseau present, session reseau : la consigne monte au besoin mesure (maison hors EVCS + voiture au plafond)',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: {
|
||||||
|
...VEHICLE_NIGHT,
|
||||||
|
soc_percent: '50',
|
||||||
|
allow_grid_hc_night: 'on',
|
||||||
|
grid_state: 'on',
|
||||||
|
victron_acin1_available: '1',
|
||||||
|
grid_setpoint_w: '50',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
sourceMode: 'grid_hc_allowed',
|
||||||
|
gridSetpointW: 5525,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'consigne-hors-session-retour-au-repos',
|
||||||
|
label: 'CONTREFACTUEL — meme nuit sans session reseau : la consigne redescend a la valeur de repos',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: {
|
||||||
|
...VEHICLE_NIGHT,
|
||||||
|
soc_percent: '50',
|
||||||
|
allow_grid_hc_night: 'off',
|
||||||
|
grid_state: 'off',
|
||||||
|
victron_acin1_available: '0',
|
||||||
|
grid_setpoint_w: '4000',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
gridSetpointW: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'consigne-deja-genereuse-aucune-ecriture',
|
||||||
|
label: 'Session reseau, consigne a 5600 W pour un besoin de 5295 W : deja suffisante, aucune reecriture (le bruit de la maison ne la fait pas courir)',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: {
|
||||||
|
...VEHICLE_NIGHT,
|
||||||
|
soc_percent: '50',
|
||||||
|
allow_grid_hc_night: 'on',
|
||||||
|
grid_state: 'on',
|
||||||
|
victron_acin1_available: '1',
|
||||||
|
grid_setpoint_w: '5600',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
gridSetpointW: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'consigne-insuffisante-corrigee-immediatement',
|
||||||
|
label: "LE DEFAUT DE LA NUIT DU 01/09 — consigne 45 W sous le besoin : la batterie porterait la difference, correction sans attendre",
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: {
|
||||||
|
...VEHICLE_NIGHT,
|
||||||
|
soc_percent: '50',
|
||||||
|
allow_grid_hc_night: 'on',
|
||||||
|
grid_state: 'on',
|
||||||
|
victron_acin1_available: '1',
|
||||||
|
grid_setpoint_w: '5250',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
sourceMode: 'grid_hc_allowed',
|
||||||
|
gridSetpointW: 5525,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'consigne-derive-externe-corrigee',
|
||||||
|
label: 'Hors session mais consigne laissee a 4000 W par un tiers : corrigee au cycle suivant',
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
soc_percent: '60',
|
||||||
|
grid_state: 'off',
|
||||||
|
victron_acin1_available: '0',
|
||||||
|
grid_setpoint_w: '4000',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
gridSetpointW: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
119
tools/analyse-test-alterne.mjs
Normal file
119
tools/analyse-test-alterne.mjs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Resume par phase d'un journal produit par tools/test-edf-alterne.mjs.
|
||||||
|
// Usage: node tools/analyse-test-alterne.mjs docs/mesures/<fichier>.log [secondesIgnorees]
|
||||||
|
//
|
||||||
|
// Les N premieres secondes de chaque phase sont ignorees par defaut (90 s) :
|
||||||
|
// la bascule du contacteur met ~30 a 90 s a s'etablir, et inclure le transitoire
|
||||||
|
// ecraserait l'ecart qu'on cherche a mesurer.
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
const file = process.argv[2];
|
||||||
|
const SKIP = Number(process.argv[3] ?? 90);
|
||||||
|
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const l of lines) {
|
||||||
|
const m = l.match(
|
||||||
|
// Le bloc des modes MPPT est le DERNIER champ : on l'ancre sur "edf on|off",
|
||||||
|
// jamais sur le mot MPPT. Ancrer sur "MPPT " faisait tomber en silence toute
|
||||||
|
// ligne dont le PREMIER tracker etait bride -- c'est-a-dire precisement les
|
||||||
|
// echantillons a mesurer -- et biaisait le resultat vers "aucun bridage".
|
||||||
|
/^(\d{2}:\d{2}:\d{2}) \| (\w+)\s*\| PV\s+(-?\d+) \| U\s+([\d.]+) \| I\s+(-?[\d.]+) \| conso\s+(-?\d+) \| grid\s+(-?\d+) \| SOC\s+(\d+).*\| edf (?:on|off) \| (.*)$/
|
||||||
|
);
|
||||||
|
if (!m) continue;
|
||||||
|
const [, t, phase, pv, u, i, conso, grid, soc, modes] = m;
|
||||||
|
rows.push({
|
||||||
|
t, phase,
|
||||||
|
pv: +pv, u: +u, i: +i, conso: +conso, grid: +grid, soc: +soc,
|
||||||
|
lim: modes.trim().split(/\s+/).filter((m) => m !== 'MPPT').length,
|
||||||
|
sec: (+t.slice(0, 2)) * 3600 + (+t.slice(3, 5)) * 60 + (+t.slice(6, 8)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = [];
|
||||||
|
const byPhase = new Map();
|
||||||
|
for (const r of rows) {
|
||||||
|
if (!byPhase.has(r.phase)) { byPhase.set(r.phase, []); order.push(r.phase); }
|
||||||
|
byPhase.get(r.phase).push(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
const avg = (a, k) => (a.length ? a.reduce((s, x) => s + x[k], 0) / a.length : NaN);
|
||||||
|
const n = (x, d = 0) => (Number.isFinite(x) ? x.toFixed(d) : '—');
|
||||||
|
|
||||||
|
console.log(`Journal : ${file} (${rows.length} echantillons, ${SKIP}s de transitoire ignorees par phase)\n`);
|
||||||
|
console.log('phase n | PV moy | PV max | conso | grid | I bat | SOC | trackers brides');
|
||||||
|
console.log('---------------------------------------------------------------------------------');
|
||||||
|
const summary = [];
|
||||||
|
for (const p of order) {
|
||||||
|
const all = byPhase.get(p);
|
||||||
|
const t0 = all[0].sec;
|
||||||
|
const a = all.filter((r) => r.sec - t0 >= SKIP);
|
||||||
|
const use = a.length ? a : all;
|
||||||
|
const s = {
|
||||||
|
phase: p, n: use.length,
|
||||||
|
pv: avg(use, 'pv'), pvmax: Math.max(...use.map((r) => r.pv)),
|
||||||
|
conso: avg(use, 'conso'), grid: avg(use, 'grid'), i: avg(use, 'i'),
|
||||||
|
soc: avg(use, 'soc'), lim: avg(use, 'lim'),
|
||||||
|
};
|
||||||
|
summary.push(s);
|
||||||
|
console.log(
|
||||||
|
`${p.padEnd(8)}${String(s.n).padStart(3)} |${n(s.pv).padStart(9)} |${n(s.pvmax).padStart(8)} |` +
|
||||||
|
`${n(s.conso).padStart(8)} |${n(s.grid).padStart(7)} |${n(s.i, 1).padStart(7)} |${n(s.soc, 1).padStart(5)} |` +
|
||||||
|
`${n(s.lim, 1).padStart(9)} / 7`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const off = summary.filter((s) => !s.phase.startsWith('ON'));
|
||||||
|
const on = summary.filter((s) => s.phase.startsWith('ON'));
|
||||||
|
|
||||||
|
// --- garde meteo -------------------------------------------------------------
|
||||||
|
// Les phases OUVERTES sont toutes censees voir la meme ressource solaire. Si
|
||||||
|
// elles ne s'accordent pas entre elles, c'est le ciel qui a change pendant la
|
||||||
|
// campagne : la moyenne ON contre moyenne OFF devient ininterpretable, et il ne
|
||||||
|
// faut surtout pas la presenter comme un resultat (piege rencontre le 27/08 au
|
||||||
|
// premier test, et de nouveau le 31/08).
|
||||||
|
const pvOffs = off.map((s) => s.pv);
|
||||||
|
const spread = Math.max(...pvOffs) - Math.min(...pvOffs);
|
||||||
|
const spreadPct = (spread / (pvOffs.reduce((a, b) => a + b, 0) / pvOffs.length)) * 100;
|
||||||
|
|
||||||
|
console.log('\n--- STABILITE DE LA RESSOURCE ---');
|
||||||
|
console.log(`PV moyenne des phases OUVERTES : ${off.map((s) => `${s.phase} ${n(s.pv)} W`).join(' | ')}`);
|
||||||
|
console.log(`Ecart entre phases ouvertes : ${n(spread)} W (${n(spreadPct, 1)} % de la moyenne)`);
|
||||||
|
const meteoStable = spreadPct < 15;
|
||||||
|
console.log(meteoStable
|
||||||
|
? '=> ressource stable : la comparaison globale ON/OFF est exploitable.'
|
||||||
|
: '=> RESSOURCE INSTABLE (nuages) : la comparaison globale ON/OFF n est PAS exploitable.\n Se fier uniquement aux transitions ci-dessous, phase par phase.');
|
||||||
|
|
||||||
|
// --- comparaison locale : chaque ON contre ses voisines OFF -------------------
|
||||||
|
console.log('\n--- TRANSITIONS (chaque phase fermee contre ses voisines ouvertes) ---');
|
||||||
|
for (const s of on) {
|
||||||
|
const idx = order.indexOf(s.phase);
|
||||||
|
const voisins = summary.filter((x) => Math.abs(order.indexOf(x.phase) - idx) === 1 && !x.phase.startsWith('ON'));
|
||||||
|
if (!voisins.length) continue;
|
||||||
|
const ref = voisins.reduce((a, x) => a + x.pv, 0) / voisins.length;
|
||||||
|
const d = s.pv - ref;
|
||||||
|
const pct = (d / ref) * 100;
|
||||||
|
console.log(
|
||||||
|
`${s.phase} : PV ${n(s.pv)} W contre ${n(ref)} W chez ${voisins.map((v) => v.phase).join('+')}` +
|
||||||
|
` => ${d >= 0 ? '+' : ''}${n(d)} W (${pct >= 0 ? '+' : ''}${n(pct, 1)} %)` +
|
||||||
|
` | import ${n(s.grid)} W | trackers brides ${n(s.lim, 1)}/7`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n--- VERDICT ---');
|
||||||
|
if (!meteoStable) {
|
||||||
|
console.log('Campagne partiellement inexploitable (ressource instable).');
|
||||||
|
console.log('Verifier a la main, sur le journal brut, si la chute de PV precede ou suit');
|
||||||
|
console.log('l apparition de l import reseau : si elle la PRECEDE, ce sont des nuages.');
|
||||||
|
} else {
|
||||||
|
const pvOff = pvOffs.reduce((a, b) => a + b, 0) / pvOffs.length;
|
||||||
|
const pvOn = on.reduce((a, s) => a + s.pv, 0) / on.length;
|
||||||
|
const gOn = on.reduce((a, s) => a + s.grid, 0) / on.length;
|
||||||
|
const pct = ((pvOn - pvOff) / pvOff) * 100;
|
||||||
|
console.log(`PV ouverte ${n(pvOff)} W | PV fermee ${n(pvOn)} W | ecart ${pct >= 0 ? '+' : ''}${n(pct, 1)} %`);
|
||||||
|
console.log(Math.abs(pct) < 10
|
||||||
|
? '=> PAS DE SUBSTITUTION : la production ne depend pas de la presence du reseau.'
|
||||||
|
: '=> SUBSTITUTION : la production chute quand le reseau est la.');
|
||||||
|
console.log(`Import moyen en phase fermee : ${n(gOn)} W (consigne ESS ~50 W attendue si le solaire couvre tout).`);
|
||||||
|
}
|
||||||
228
tools/etude-frontiere-excedent.mjs
Normal file
228
tools/etude-frontiere-excedent.mjs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Étude de la frontière « excédent » pour la charge du véhicule.
|
||||||
|
//
|
||||||
|
// Question posée par le propriétaire (31/08/2026) : la voiture doit être un
|
||||||
|
// délestage de l'excédent PV, pas une charge prioritaire qui vide les
|
||||||
|
// batteries. La frontière se détecte à la PUISSANCE DE DÉCHARGE BATTERIE,
|
||||||
|
// pas à la production PV ni à un seuil de SOC.
|
||||||
|
//
|
||||||
|
// Ce script ne décide rien et n'écrit rien. Il intègre l'historique HA
|
||||||
|
// (résolution native ~2 s) et répartit l'énergie réellement livrée au
|
||||||
|
// véhicule entre trois origines physiques, instant par instant :
|
||||||
|
//
|
||||||
|
// batterie : min(P_evcs, décharge batterie) -> réserve entamée
|
||||||
|
// réseau : min(reste, import réseau) -> énergie payante
|
||||||
|
// excédent : le solde -> production qui aurait
|
||||||
|
// été refusée ou stockée
|
||||||
|
//
|
||||||
|
// Usage: node tools/etude-frontiere-excedent.mjs [debut] [fin]
|
||||||
|
// node tools/etude-frontiere-excedent.mjs 2026-08-24 2026-09-01
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const HA_URL = process.env.HA_URL || 'http://10.10.10.17:8123';
|
||||||
|
|
||||||
|
function loadToken() {
|
||||||
|
if (process.env.HA_TOKEN) return process.env.HA_TOKEN;
|
||||||
|
const envPath = path.join(ROOT, '.env');
|
||||||
|
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
|
||||||
|
const m = line.match(/^\s*HA_TOKEN\s*=\s*(.+?)\s*$/);
|
||||||
|
if (m) return m[1].replace(/^["']|["']$/g, '');
|
||||||
|
}
|
||||||
|
console.error('HA_TOKEN absent de .env');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const TOKEN = loadToken();
|
||||||
|
|
||||||
|
const ENT = {
|
||||||
|
evcs: 'sensor.victron_evcs_ac_power',
|
||||||
|
batt: 'sensor.victron_battery_power',
|
||||||
|
pv: 'sensor.victron_pv_power_system',
|
||||||
|
conso:'sensor.victron_consumption_power_l1',
|
||||||
|
grid: 'sensor.victron_grid_power_l1',
|
||||||
|
soc: 'sensor.victron_battery_soc',
|
||||||
|
};
|
||||||
|
const KEYS = Object.keys(ENT);
|
||||||
|
|
||||||
|
async function fetchDay(dayStart, dayEnd) {
|
||||||
|
const url =
|
||||||
|
`${HA_URL}/api/history/period/${dayStart.toISOString()}` +
|
||||||
|
`?end_time=${encodeURIComponent(dayEnd.toISOString())}` +
|
||||||
|
`&filter_entity_id=${encodeURIComponent(Object.values(ENT).join(','))}` +
|
||||||
|
`&minimal_response`;
|
||||||
|
const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status} sur ${dayStart.toISOString()}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fusionne les séries en un flux d'événements trié.
|
||||||
|
function toEvents(series) {
|
||||||
|
const byEntity = new Map();
|
||||||
|
for (const rows of series) {
|
||||||
|
if (!rows.length) continue;
|
||||||
|
byEntity.set(rows[0].entity_id, rows);
|
||||||
|
}
|
||||||
|
const ev = [];
|
||||||
|
for (const [key, id] of Object.entries(ENT)) {
|
||||||
|
for (const r of byEntity.get(id) || []) {
|
||||||
|
const v = Number(r.state);
|
||||||
|
if (!Number.isFinite(v)) continue;
|
||||||
|
ev.push({ t: Date.parse(r.last_changed || r.last_updated), key, v });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ev.sort((a, b) => a.t - b.t);
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_GAP_MS = 300e3; // au-delà, on considère une lacune et on n'intègre pas
|
||||||
|
|
||||||
|
const HOURS = 24;
|
||||||
|
function blankDay() {
|
||||||
|
return {
|
||||||
|
evcsKwh: 0, fromBattKwh: 0, fromGridKwh: 0, fromSurplusKwh: 0,
|
||||||
|
pvKwh: 0, consoKwh: 0, gridImportKwh: 0, battDischargeKwh: 0,
|
||||||
|
gapSec: 0,
|
||||||
|
hour: Array.from({ length: HOURS }, () => ({
|
||||||
|
evcsKwh: 0, fromBattKwh: 0, fromGridKwh: 0, fromSurplusKwh: 0,
|
||||||
|
})),
|
||||||
|
socMin: null, socMax: null,
|
||||||
|
lastSurplusMs: null, // dernier instant ou la voiture chargeait SANS decharger la batterie
|
||||||
|
firstSurplusMs: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const endArg = args[1] ? new Date(args[1]) : new Date();
|
||||||
|
const startArg = args[0] ? new Date(args[0]) : new Date(endArg.getTime() - 8 * 864e5);
|
||||||
|
|
||||||
|
const days = new Map(); // 'YYYY-MM-DD' (Paris) -> agrégats
|
||||||
|
|
||||||
|
const parisParts = (ms) => {
|
||||||
|
const d = new Date(ms);
|
||||||
|
const f = new Intl.DateTimeFormat('fr-CA', {
|
||||||
|
timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit',
|
||||||
|
day: '2-digit', hour: '2-digit', hour12: false,
|
||||||
|
}).formatToParts(d);
|
||||||
|
const g = (t) => f.find((p) => p.type === t).value;
|
||||||
|
return { day: `${g('year')}-${g('month')}-${g('day')}`, hour: Number(g('hour')) % 24 };
|
||||||
|
};
|
||||||
|
|
||||||
|
let cursor = new Date(startArg);
|
||||||
|
const cur = {}; // dernières valeurs connues
|
||||||
|
let lastT = null;
|
||||||
|
|
||||||
|
while (cursor < endArg) {
|
||||||
|
const chunkEnd = new Date(Math.min(cursor.getTime() + 864e5, endArg.getTime()));
|
||||||
|
process.stderr.write(` lecture ${cursor.toISOString().slice(0, 10)}...\n`);
|
||||||
|
const ev = toEvents(await fetchDay(cursor, chunkEnd));
|
||||||
|
|
||||||
|
for (const e of ev) {
|
||||||
|
if (lastT !== null) {
|
||||||
|
const dtMs = e.t - lastT;
|
||||||
|
if (dtMs > 0) {
|
||||||
|
const { day, hour } = parisParts(lastT);
|
||||||
|
if (!days.has(day)) days.set(day, blankDay());
|
||||||
|
const D = days.get(day);
|
||||||
|
if (dtMs > MAX_GAP_MS) {
|
||||||
|
D.gapSec += dtMs / 1000;
|
||||||
|
} else if (KEYS.every((k) => cur[k] !== undefined)) {
|
||||||
|
const h = dtMs / 3.6e6; // heures
|
||||||
|
const evcs = Math.max(0, cur.evcs);
|
||||||
|
const battDis = Math.max(0, -cur.batt); // W déchargés
|
||||||
|
const gridImp = Math.max(0, cur.grid); // W importés
|
||||||
|
|
||||||
|
const fromBatt = Math.min(evcs, battDis);
|
||||||
|
const fromGrid = Math.min(evcs - fromBatt, gridImp);
|
||||||
|
const fromSurp = Math.max(0, evcs - fromBatt - fromGrid);
|
||||||
|
|
||||||
|
D.evcsKwh += evcs * h / 1000;
|
||||||
|
D.fromBattKwh += fromBatt * h / 1000;
|
||||||
|
D.fromGridKwh += fromGrid * h / 1000;
|
||||||
|
D.fromSurplusKwh += fromSurp * h / 1000;
|
||||||
|
D.pvKwh += Math.max(0, cur.pv) * h / 1000;
|
||||||
|
D.consoKwh += Math.max(0, cur.conso) * h / 1000;
|
||||||
|
D.gridImportKwh += gridImp * h / 1000;
|
||||||
|
D.battDischargeKwh += battDis * h / 1000;
|
||||||
|
|
||||||
|
if (evcs > 200 && fromSurp > 0.6 * evcs) {
|
||||||
|
if (D.firstSurplusMs === null) D.firstSurplusMs = lastT;
|
||||||
|
D.lastSurplusMs = lastT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const H = D.hour[hour];
|
||||||
|
H.evcsKwh += evcs * h / 1000;
|
||||||
|
H.fromBattKwh += fromBatt * h / 1000;
|
||||||
|
H.fromGridKwh += fromGrid * h / 1000;
|
||||||
|
H.fromSurplusKwh += fromSurp * h / 1000;
|
||||||
|
}
|
||||||
|
if (cur.soc !== undefined) {
|
||||||
|
D.socMin = D.socMin === null ? cur.soc : Math.min(D.socMin, cur.soc);
|
||||||
|
D.socMax = D.socMax === null ? cur.soc : Math.max(D.socMax, cur.soc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur[e.key] = e.v;
|
||||||
|
lastT = e.t;
|
||||||
|
}
|
||||||
|
cursor = chunkEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r1 = (x) => x.toFixed(1);
|
||||||
|
const r2 = (x) => x.toFixed(2);
|
||||||
|
|
||||||
|
console.log('\n=== ORIGINE DE L\'ENERGIE LIVREE AU VEHICULE (kWh/jour) ===\n');
|
||||||
|
console.log('jour EVCS excedent batterie reseau | PV conso import SOC');
|
||||||
|
let tot = { e: 0, s: 0, b: 0, g: 0 };
|
||||||
|
const sorted = [...days.keys()].sort();
|
||||||
|
for (const d of sorted) {
|
||||||
|
const D = days.get(d);
|
||||||
|
if (D.gapSec > 3600) console.log(` (lacune historique ${Math.round(D.gapSec / 60)} min le ${d})`);
|
||||||
|
tot.e += D.evcsKwh; tot.s += D.fromSurplusKwh; tot.b += D.fromBattKwh; tot.g += D.fromGridKwh;
|
||||||
|
console.log(
|
||||||
|
`${d} ${r2(D.evcsKwh).padStart(6)} ${r2(D.fromSurplusKwh).padStart(7)} ` +
|
||||||
|
`${r2(D.fromBattKwh).padStart(7)} ${r2(D.fromGridKwh).padStart(7)} | ` +
|
||||||
|
`${r1(D.pvKwh).padStart(5)} ${r1(D.consoKwh).padStart(6)} ${r1(D.gridImportKwh).padStart(6)} ` +
|
||||||
|
`${D.socMin ?? '?'}-${D.socMax ?? '?'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`\nTOTAL ${r2(tot.e).padStart(6)} ${r2(tot.s).padStart(7)} ${r2(tot.b).padStart(7)} ${r2(tot.g).padStart(7)}`);
|
||||||
|
if (tot.e > 0) {
|
||||||
|
console.log(`part 100% ${(100 * tot.s / tot.e).toFixed(0).padStart(6)}% ` +
|
||||||
|
`${(100 * tot.b / tot.e).toFixed(0).padStart(6)}% ${(100 * tot.g / tot.e).toFixed(0).padStart(6)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hhmm = (ms) => ms === null ? ' -- ' : new Date(ms).toLocaleTimeString('fr-FR',
|
||||||
|
{ timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' });
|
||||||
|
|
||||||
|
console.log('\n=== FRONTIERE DE L\'EXCEDENT, JOUR PAR JOUR ===\n');
|
||||||
|
console.log('jour ouverture fermeture SOC max part excedent 16h 17h 18h 19h 20h');
|
||||||
|
for (const d of sorted) {
|
||||||
|
const D = days.get(d);
|
||||||
|
const sh = (h) => {
|
||||||
|
const H = D.hour[h];
|
||||||
|
return H.evcsKwh < 0.05 ? ' . ' : `${(100 * H.fromSurplusKwh / H.evcsKwh).toFixed(0).padStart(3)}%`;
|
||||||
|
};
|
||||||
|
console.log(
|
||||||
|
`${d} ${hhmm(D.firstSurplusMs)} ${hhmm(D.lastSurplusMs)} ` +
|
||||||
|
`${String(D.socMax ?? '?').padStart(3)}% ` +
|
||||||
|
`${sh(16)} ${sh(17)} ${sh(18)} ${sh(19)} ${sh(20)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== PAR HEURE DE LA JOURNEE, cumul sur la periode (kWh) ===\n');
|
||||||
|
console.log('h EVCS excedent batterie reseau part excedent');
|
||||||
|
for (let h = 0; h < HOURS; h++) {
|
||||||
|
let e = 0, s = 0, b = 0, g = 0;
|
||||||
|
for (const d of sorted) {
|
||||||
|
const H = days.get(d).hour[h];
|
||||||
|
e += H.evcsKwh; s += H.fromSurplusKwh; b += H.fromBattKwh; g += H.fromGridKwh;
|
||||||
|
}
|
||||||
|
if (e < 0.01) continue;
|
||||||
|
console.log(
|
||||||
|
`${String(h).padStart(2)}h ${r2(e).padStart(7)} ${r2(s).padStart(7)} ${r2(b).padStart(7)} ` +
|
||||||
|
`${r2(g).padStart(7)} ${(100 * s / e).toFixed(0).padStart(3)}%`
|
||||||
|
);
|
||||||
|
}
|
||||||
134
tools/ha-backup-config.mjs
Normal file
134
tools/ha-backup-config.mjs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Configure la sauvegarde automatique de Home Assistant (installation Core /
|
||||||
|
* Container, HA 2026.8.1 — pas de Supervisor ici).
|
||||||
|
*
|
||||||
|
* POURQUOI CE SCRIPT PLUTÔT QUE L'INTERFACE
|
||||||
|
* La configuration de sauvegarde n'est PAS exposée par l'API REST : elle vit
|
||||||
|
* dans `.storage/backup` et ne se règle que par l'interface ou par le
|
||||||
|
* websocket. Passer par le websocket la rend reproductible, versionnée, et
|
||||||
|
* relisible dans six mois — ce qu'un clic dans l'interface n'est pas.
|
||||||
|
*
|
||||||
|
* CE QUI EST RÉGLÉ, ET POURQUOI
|
||||||
|
* - Destinations : les agents Synology **ARCHIVE2 (10.10.10.22)** et
|
||||||
|
* **ARCHIVE (10.10.10.50)**, plus une copie locale. Le NAS principal
|
||||||
|
* (ARCHIVE3, 10.10.10.17) est délibérément EXCLU comme destination : une
|
||||||
|
* sauvegarde qui ne survit pas à la panne qu'elle couvre ne sert à rien.
|
||||||
|
* - Base de données EXCLUE. Ce qu'il faut pouvoir restaurer vite, c'est
|
||||||
|
* l'identité du système (entités, helpers, cartes, paquets, intégrations) :
|
||||||
|
* environ 60 Mo. L'historique, lui, pèse 4,4 Go, change en permanence, et
|
||||||
|
* n'a aucune valeur d'urgence — il se sauvegarde séparément, plus rarement,
|
||||||
|
* et avec les précautions SQLite (mode WAL : jamais un `cp` à chaud).
|
||||||
|
* - Rétention 7 copies : une semaine pour s'apercevoir d'une bêtise.
|
||||||
|
* - Pas de mot de passe. Choix assumé : ces archives restent sur le réseau
|
||||||
|
* local du propriétaire, et une sauvegarde restaurable vaut mieux qu'une
|
||||||
|
* sauvegarde chiffrée dont la clé est perdue le jour où elle sert.
|
||||||
|
*
|
||||||
|
* Usage :
|
||||||
|
* node tools/ha-backup-config.mjs --dry-run (affiche avant/après, n'écrit rien)
|
||||||
|
* node tools/ha-backup-config.mjs (applique)
|
||||||
|
* node tools/ha-backup-config.mjs --run-now (applique puis déclenche une sauvegarde)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const HA_URL = process.env.HA_URL || 'http://10.10.10.17:8123';
|
||||||
|
const ARGS = new Set(process.argv.slice(2));
|
||||||
|
const DRY = ARGS.has('--dry-run');
|
||||||
|
const RUN_NOW = ARGS.has('--run-now');
|
||||||
|
|
||||||
|
function loadToken() {
|
||||||
|
if (process.env.HA_TOKEN) return process.env.HA_TOKEN;
|
||||||
|
const m = fs.readFileSync(path.join(ROOT, '.env'), 'utf8').match(/^\s*HA_TOKEN\s*=\s*(.+?)\s*$/m);
|
||||||
|
if (!m) { console.error('HA_TOKEN absent de .env'); process.exit(1); }
|
||||||
|
return m[1].replace(/^["']|["']$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agents de destination. Identifiants relevés via `backup/agents/info` et
|
||||||
|
// croisés avec les entrées `synology_dsm` de `.storage/core.config_entries`
|
||||||
|
// pour connaître l'hôte réel de chacun — le nom seul ne dit pas où il écrit.
|
||||||
|
const AGENTS = {
|
||||||
|
local: 'backup.local', // 10.10.10.17, restauration rapide
|
||||||
|
archive2: 'synology_dsm.1660NZN396007', // 10.10.10.22 — second Synology
|
||||||
|
archive: 'synology_dsm.14B0MPN914514', // 10.10.10.50 — troisième machine
|
||||||
|
// 'synology_dsm.20A0SBRVM9PF2' = ARCHIVE3 = 10.10.10.17 : NE PAS UTILISER
|
||||||
|
// comme destination, c'est la machine dont on se protège.
|
||||||
|
};
|
||||||
|
|
||||||
|
const DESIRED = {
|
||||||
|
create_backup: {
|
||||||
|
agent_ids: [AGENTS.local, AGENTS.archive2, AGENTS.archive],
|
||||||
|
include_database: false,
|
||||||
|
include_folders: null,
|
||||||
|
include_all_addons: false,
|
||||||
|
include_addons: null,
|
||||||
|
name: null,
|
||||||
|
password: null,
|
||||||
|
},
|
||||||
|
retention: { copies: 7, days: null },
|
||||||
|
schedule: { recurrence: 'daily', time: '04:45:00', days: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = loadToken();
|
||||||
|
const ws = new WebSocket(HA_URL.replace(/^http/, 'ws') + '/api/websocket');
|
||||||
|
let seq = 1;
|
||||||
|
const pending = new Map();
|
||||||
|
|
||||||
|
function call(msg) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = seq++;
|
||||||
|
pending.set(id, { resolve, reject });
|
||||||
|
ws.send(JSON.stringify({ id, ...msg }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = async (ev) => {
|
||||||
|
const m = JSON.parse(ev.data);
|
||||||
|
if (m.type === 'auth_required') return ws.send(JSON.stringify({ type: 'auth', access_token: token }));
|
||||||
|
if (m.type === 'auth_invalid') { console.error('jeton HA refusé'); process.exit(1); }
|
||||||
|
if (m.type === 'auth_ok') return main().catch(e => { console.error(e.message); process.exit(1); });
|
||||||
|
if (m.type === 'result') {
|
||||||
|
const p = pending.get(m.id); pending.delete(m.id);
|
||||||
|
if (!p) return;
|
||||||
|
m.success ? p.resolve(m.result) : p.reject(new Error(JSON.stringify(m.error)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onerror = () => { console.error(`websocket injoignable sur ${HA_URL}`); process.exit(1); };
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const before = (await call({ type: 'backup/config/info' })).config;
|
||||||
|
console.log('=== AVANT ===');
|
||||||
|
console.log(` configurée : ${before.automatic_backups_configured}`);
|
||||||
|
console.log(` récurrence : ${before.schedule.recurrence} à ${before.schedule.time ?? '—'}`);
|
||||||
|
console.log(` destinations : ${before.create_backup.agent_ids.join(', ') || '— aucune —'}`);
|
||||||
|
console.log(` base incluse : ${before.create_backup.include_database}`);
|
||||||
|
console.log(` rétention : ${before.retention.copies ?? '—'} copies`);
|
||||||
|
|
||||||
|
console.log('\n=== APRÈS (souhaité) ===');
|
||||||
|
console.log(` récurrence : ${DESIRED.schedule.recurrence} à ${DESIRED.schedule.time}`);
|
||||||
|
console.log(' destinations :');
|
||||||
|
console.log(` ${AGENTS.local} → 10.10.10.17 (copie locale, restauration rapide)`);
|
||||||
|
console.log(` ${AGENTS.archive2} → 10.10.10.22 (second Synology)`);
|
||||||
|
console.log(` ${AGENTS.archive} → 10.10.10.50 (troisième machine)`);
|
||||||
|
console.log(` base incluse : ${DESIRED.create_backup.include_database} (les 4,4 Go d'historique se sauvegardent à part)`);
|
||||||
|
console.log(` rétention : ${DESIRED.retention.copies} copies`);
|
||||||
|
|
||||||
|
if (DRY) { console.log('\n[DRY-RUN] aucune écriture.'); ws.close(); process.exit(0); }
|
||||||
|
|
||||||
|
await call({ type: 'backup/config/update', ...DESIRED });
|
||||||
|
const after = (await call({ type: 'backup/config/info' })).config;
|
||||||
|
console.log(`\n✅ appliqué — configurée=${after.automatic_backups_configured}, ` +
|
||||||
|
`prochaine=${after.next_automatic_backup ?? '?'}`);
|
||||||
|
|
||||||
|
if (RUN_NOW) {
|
||||||
|
console.log('\nDéclenchement d\'une sauvegarde de vérification…');
|
||||||
|
await call({ type: 'backup/generate_with_automatic_settings' });
|
||||||
|
console.log(' demandée. Suivre avec: node tools/ha-backup-list.mjs');
|
||||||
|
}
|
||||||
|
ws.close(); process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => { console.error('délai dépassé'); process.exit(1); }, 60000);
|
||||||
48
tools/ha-backup-list.mjs
Normal file
48
tools/ha-backup-list.mjs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Liste les sauvegardes Home Assistant et, pour chacune, les destinations où
|
||||||
|
// elle est RÉELLEMENT présente. C'est cette dernière colonne qui compte : une
|
||||||
|
// sauvegarde qui n'existe que sur la machine dont on se protège ne protège rien.
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const HA_URL = process.env.HA_URL || 'http://10.10.10.17:8123';
|
||||||
|
const token = process.env.HA_TOKEN ||
|
||||||
|
fs.readFileSync(path.join(ROOT, '.env'), 'utf8').match(/^\s*HA_TOKEN\s*=\s*(.+?)\s*$/m)[1].replace(/^["']|["']$/g, '');
|
||||||
|
|
||||||
|
const HOST = {
|
||||||
|
'backup.local': '10.10.10.17 (local)',
|
||||||
|
'synology_dsm.1660NZN396007': '10.10.10.22',
|
||||||
|
'synology_dsm.14B0MPN914514': '10.10.10.50',
|
||||||
|
'synology_dsm.20A0SBRVM9PF2': '10.10.10.17',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ws = new WebSocket(HA_URL.replace(/^http/, 'ws') + '/api/websocket');
|
||||||
|
ws.onmessage = (ev) => {
|
||||||
|
const m = JSON.parse(ev.data);
|
||||||
|
if (m.type === 'auth_required') return ws.send(JSON.stringify({ type: 'auth', access_token: token }));
|
||||||
|
if (m.type === 'auth_ok') return ws.send(JSON.stringify({ id: 1, type: 'backup/info' }));
|
||||||
|
if (m.type === 'result') {
|
||||||
|
const r = m.result || {};
|
||||||
|
if (r.backups?.length) {
|
||||||
|
for (const b of r.backups) {
|
||||||
|
const mo = (b.agents ? Object.entries(b.agents) : []);
|
||||||
|
console.log(`\n${b.name} (${b.backup_id})`);
|
||||||
|
console.log(` créée : ${new Date(b.date).toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })}`);
|
||||||
|
console.log(` base incluse : ${b.database_included} automatique : ${b.with_automatic_settings}`);
|
||||||
|
for (const [agent, info] of mo) {
|
||||||
|
const mb = info.size ? (info.size / 1048576).toFixed(1) + ' Mo' : '?';
|
||||||
|
console.log(` → ${(HOST[agent] || agent).padEnd(22)} ${mb.padStart(9)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Aucune sauvegarde.');
|
||||||
|
}
|
||||||
|
if (r.next_automatic_backup) {
|
||||||
|
console.log(`\nProchaine automatique : ${new Date(r.next_automatic_backup).toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })}`);
|
||||||
|
}
|
||||||
|
ws.close(); process.exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(() => { console.error('délai dépassé'); process.exit(1); }, 30000);
|
||||||
157
tools/test-edf-alterne.mjs
Normal file
157
tools/test-edf-alterne.mjs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Campagne de test EDF <-> MPPT, protocole ALTERNE.
|
||||||
|
//
|
||||||
|
// Usage: node tools/test-edf-alterne.mjs [phaseSec] [sampleSec]
|
||||||
|
// node tools/test-edf-alterne.mjs 180 15 (defaut)
|
||||||
|
//
|
||||||
|
// Protocole : OFF / ON / OFF / ON / OFF, phases de duree egale, echantillon
|
||||||
|
// regulier. L'alternance est ce qui rend la mesure interpretable : une derive
|
||||||
|
// meteo monotone ne peut pas produire un signal qui suit le contacteur quatre
|
||||||
|
// fois de suite.
|
||||||
|
//
|
||||||
|
// Ce que le script mesure a chaque echantillon : PV totale, tension et courant
|
||||||
|
// batterie, consommation maison, import reseau, SOC, limite DVCC, presence
|
||||||
|
// physique AC-IN, et le mode de fonctionnement des 7 chargeurs solaires
|
||||||
|
// (mppt_active = cherche son point de puissance maximale ; sinon il se retient).
|
||||||
|
//
|
||||||
|
// SECURITE : l'hysteresis reseau est desactivee pendant le test puis restauree,
|
||||||
|
// ainsi que le latch et l'etat du contacteur. La restauration est faite aussi
|
||||||
|
// sur Ctrl-C et sur erreur.
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const HA = process.env.HA_URL || 'http://10.10.10.17:8123';
|
||||||
|
|
||||||
|
function loadToken() {
|
||||||
|
if (process.env.HA_TOKEN) return process.env.HA_TOKEN;
|
||||||
|
const m = fs.readFileSync(path.join(ROOT, '.env'), 'utf8').match(/^\s*HA_TOKEN\s*=\s*(.+?)\s*$/m);
|
||||||
|
if (!m) { console.error('HA_TOKEN absent de .env'); process.exit(1); }
|
||||||
|
return m[1].replace(/^["']|["']$/g, '');
|
||||||
|
}
|
||||||
|
const TOKEN = loadToken();
|
||||||
|
const H = { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' };
|
||||||
|
|
||||||
|
const get = async (id) => {
|
||||||
|
const r = await fetch(`${HA}/api/states/${id}`, { headers: H });
|
||||||
|
if (!r.ok) return null;
|
||||||
|
return (await r.json()).state;
|
||||||
|
};
|
||||||
|
const call = async (svc, data) => {
|
||||||
|
const [d, s] = svc.split('.');
|
||||||
|
const r = await fetch(`${HA}/api/services/${d}/${s}`, { method: 'POST', headers: H, body: JSON.stringify(data) });
|
||||||
|
if (!r.ok) throw new Error(`${svc} -> HTTP ${r.status} ${await r.text()}`);
|
||||||
|
};
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
// --- entites -----------------------------------------------------------------
|
||||||
|
const MPPT = [
|
||||||
|
['mppt1', 'sensor.airtria_mppt1_mppt_operation_mode'],
|
||||||
|
['rs_1', 'sensor.mppt_450_200_hq2449yr9pt_pv_tracker_solar_1_operation_mode'],
|
||||||
|
['rs_2', 'sensor.mppt_450_200_hq2449yr9pt_pv_tracker_solar_2_operation_mode'],
|
||||||
|
['rs_3', 'sensor.mppt_450_200_hq2449yr9pt_pv_tracker_solar_3_operation_mode'],
|
||||||
|
['rs_4', 'sensor.mppt_450_200_hq2449yr9pt_pv_tracker_solar_4_operation_mode'],
|
||||||
|
['rs_g', 'sensor.mppt_450_200_hq2449yr9pt_mppt_operation_mode'],
|
||||||
|
['v1', 'sensor.v1_mppt_operation_mode'],
|
||||||
|
];
|
||||||
|
const M = {
|
||||||
|
pv: 'sensor.victron_pv_power_system',
|
||||||
|
u: 'sensor.victron_battery_voltage',
|
||||||
|
i: 'sensor.victron_battery_current',
|
||||||
|
conso: 'sensor.victron_consumption_power_l1',
|
||||||
|
grid: 'sensor.victron_grid_power_l1',
|
||||||
|
soc: 'sensor.victron_battery_soc',
|
||||||
|
dvcc_a: 'sensor.victron_max_charge_current_setting',
|
||||||
|
dvcc_v: 'sensor.victron_max_charge_voltage_setting',
|
||||||
|
acin: 'sensor.victron_ac_in1_available',
|
||||||
|
edf: 'switch.edf_general_ok_switch',
|
||||||
|
mode: 'input_text.victron_charge_mode',
|
||||||
|
};
|
||||||
|
const HYST = 'input_boolean.victron_grid_hysteresis_enabled';
|
||||||
|
const LATCH = 'input_boolean.victron_grid_hysteresis_latch';
|
||||||
|
|
||||||
|
const PHASE_SEC = Number(process.argv[2] || 180);
|
||||||
|
const SAMPLE_SEC = Number(process.argv[3] || 15);
|
||||||
|
const PHASES = ['OFF_1', 'ON_1', 'OFF_2', 'ON_2', 'FIN_OFF'];
|
||||||
|
|
||||||
|
const stamp = () => new Date().toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris' });
|
||||||
|
const out = [];
|
||||||
|
const say = (l) => { console.log(l); out.push(l); };
|
||||||
|
|
||||||
|
// --- restauration ------------------------------------------------------------
|
||||||
|
let initial = null;
|
||||||
|
let restored = false;
|
||||||
|
async function restore(why) {
|
||||||
|
if (restored || !initial) return;
|
||||||
|
restored = true;
|
||||||
|
say(`>>> RESTAURATION (${why})`);
|
||||||
|
try {
|
||||||
|
await call('switch.turn_off', { entity_id: M.edf });
|
||||||
|
await call(`input_boolean.turn_${initial.latch}`, { entity_id: LATCH });
|
||||||
|
await call(`input_boolean.turn_${initial.hyst}`, { entity_id: HYST });
|
||||||
|
say(`>>> etat restaure : edf off | latch ${initial.latch} | hysteresis ${initial.hyst}`);
|
||||||
|
} catch (e) {
|
||||||
|
say(`!!! ECHEC RESTAURATION : ${e.message} — VERIFIER A LA MAIN ${M.edf}, ${LATCH}, ${HYST}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.on('SIGINT', async () => { await restore('Ctrl-C'); process.exit(130); });
|
||||||
|
|
||||||
|
// --- deroule -----------------------------------------------------------------
|
||||||
|
initial = {
|
||||||
|
hyst: (await get(HYST)) === 'on' ? 'on' : 'off',
|
||||||
|
latch: (await get(LATCH)) === 'on' ? 'on' : 'off',
|
||||||
|
edf: await get(M.edf),
|
||||||
|
};
|
||||||
|
|
||||||
|
say(`TEST ALTERNE EDF <-> MPPT — ${new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })}`);
|
||||||
|
say(`phases ${PHASE_SEC}s, echantillon ${SAMPLE_SEC}s, sequence ${PHASES.join(' / ')}`);
|
||||||
|
say(`etat initial : edf ${initial.edf} | latch ${initial.latch} | hysteresis ${initial.hyst}`);
|
||||||
|
say(`consigne reseau ${await get('number.gx_device_ac_power_setpoint')} W | SOC min ESS ${await get('number.gx_device_ess_min_soc_limit')} % | limite active ${await get('sensor.gx_device_active_soc_limit')} %`);
|
||||||
|
say(`MPPT ordre: ${MPPT.map((m) => m[0]).join(' ')} (MPPT=cherche le max, lim_=se retient)`);
|
||||||
|
say('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await call('input_boolean.turn_off', { entity_id: HYST });
|
||||||
|
await call('switch.turn_off', { entity_id: M.edf });
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
for (const phase of PHASES) {
|
||||||
|
const want = phase.startsWith('ON') ? 'on' : 'off';
|
||||||
|
if ((await get(M.edf)) !== want) {
|
||||||
|
say(`>>> EDF ${want.toUpperCase()}`);
|
||||||
|
await call(`switch.turn_${want}`, { entity_id: M.edf });
|
||||||
|
await sleep(3000);
|
||||||
|
}
|
||||||
|
const t0 = Date.now();
|
||||||
|
while (Date.now() - t0 < PHASE_SEC * 1000) {
|
||||||
|
const v = {};
|
||||||
|
await Promise.all(Object.entries(M).map(async ([k, id]) => { v[k] = await get(id); }));
|
||||||
|
const modes = await Promise.all(MPPT.map(async ([, id]) => {
|
||||||
|
const s = await get(id);
|
||||||
|
return s === 'mppt_active' ? 'MPPT' : (s || 'n/a').slice(0, 4).padEnd(4);
|
||||||
|
}));
|
||||||
|
const n = (x, d = 0) => (Number.isFinite(Number(x)) ? Number(x).toFixed(d) : String(x));
|
||||||
|
say(
|
||||||
|
`${stamp()} | ${phase.padEnd(7)}| PV ${n(v.pv).padStart(5)} | U ${n(v.u, 2).padStart(5)}` +
|
||||||
|
` | I ${n(v.i, 1).padStart(6)} | conso ${n(v.conso).padStart(5)} | grid ${n(v.grid).padStart(5)}` +
|
||||||
|
` | SOC ${n(v.soc)} | DVCC ${n(v.dvcc_a)}A/${n(v.dvcc_v, 1)}V | acin ${v.acin} | edf ${v.edf}` +
|
||||||
|
` | ${modes.join(' ')}`
|
||||||
|
);
|
||||||
|
await sleep(SAMPLE_SEC * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
say(`!!! ERREUR : ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
await restore('fin de campagne');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le nom porte la date ET l'heure : deux campagnes le meme jour ne doivent pas
|
||||||
|
// s'ecraser l'une l'autre (piege rencontre le 31/08).
|
||||||
|
const now = new Date();
|
||||||
|
const hhmm = now.toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' }).replace(':', '');
|
||||||
|
const dest = path.join(ROOT, 'docs/mesures', `${now.toISOString().slice(0, 10).replace(/-/g, '')}-${hhmm}-test-edf-alterne.log`);
|
||||||
|
fs.writeFileSync(dest, out.join('\n') + '\n');
|
||||||
|
console.log(`\nJournal : ${dest}`);
|
||||||
289
tools/watchdog-evcs.mjs
Normal file
289
tools/watchdog-evcs.mjs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* CHIEN DE GARDE EVCS — remise en état sûr quand le contrôleur ne répond plus.
|
||||||
|
*
|
||||||
|
* POURQUOI
|
||||||
|
* Depuis le 01/09/2026 le contrôleur EVCS pilote lui-même la consigne de
|
||||||
|
* puissance réseau de l'ESS (HANDOFF §31). C'est ce qui rend une session
|
||||||
|
* réseau réelle — mais c'est aussi un risque nouveau : s'il s'arrête EN PLEINE
|
||||||
|
* SESSION, la consigne reste haute et le réseau se substitue au solaire toute
|
||||||
|
* la journée suivante (HANDOFF §26/§28). Ce chien de garde existe d'abord pour
|
||||||
|
* ça. Il couvre au passage l'incident du 09/07/2026 (NAS éteint la nuit, EDF
|
||||||
|
* laissé fermé jusqu'au matin).
|
||||||
|
*
|
||||||
|
* OÙ IL TOURNE
|
||||||
|
* Sur le SECOND Synology (10.10.10.22), pas sur celui qui héberge HA et n8n.
|
||||||
|
* Un chien de garde hébergé par la machine qu'il surveille ne sert à rien.
|
||||||
|
*
|
||||||
|
* CE QU'IL NE FAIT PAS, ET POURQUOI
|
||||||
|
* 1. Il ne prend JAMAIS de décision d'énergie. Il ne démarre rien, ne charge
|
||||||
|
* rien, ne calcule aucun seuil. Il remet le système dans son état sûr par
|
||||||
|
* défaut et s'arrête là. Deux automates qui décident sur le même contacteur,
|
||||||
|
* c'est la pathologie que ce projet passe son temps à retirer.
|
||||||
|
* 2. Il ne touche PAS au contacteur EDF. D'abord parce que c'est un Tuya
|
||||||
|
* *cloud* (HANDOFF §32) : le commander dépend d'internet, donc ce n'est pas
|
||||||
|
* un actionneur fiable en secours. Ensuite parce que ce n'est pas
|
||||||
|
* nécessaire : avec la consigne ramenée à sa valeur de repos, l'ESS
|
||||||
|
* retrouve son autoconsommation normale — la maison passe sur la batterie
|
||||||
|
* jusqu'au SOC minimal, puis le réseau prend le relais tout seul. Contacteur
|
||||||
|
* fermé + consigne au repos est un état SÛR.
|
||||||
|
* 3. Il ne passe ni par Home Assistant ni par le cloud pour agir : il écrit en
|
||||||
|
* MQTT directement sur le GX Venus (10.10.10.9), une TROISIÈME machine,
|
||||||
|
* indépendante des deux NAS. C'est ce qui lui permet d'agir quand HA est
|
||||||
|
* mort avec le reste.
|
||||||
|
*
|
||||||
|
* SIGNAL DE VIE
|
||||||
|
* L'API d'exécutions de n8n, pas un helper HA. Vérifié le 01/09 : HA ne
|
||||||
|
* rafraîchit PAS l'horodatage d'un `input_text` réécrit à l'identique, donc
|
||||||
|
* `victron_evcs_surplus_state` paraissait figé depuis 40 min alors que le
|
||||||
|
* contrôleur tournait normalement. Un helper HA est un mauvais battement de
|
||||||
|
* cœur. Une exécution n8n horodatée en est un bon : elle prouve que la boucle
|
||||||
|
* s'exécute, et son absence couvre aussi bien « n8n arrêté » que « NAS éteint »
|
||||||
|
* que « réseau coupé ».
|
||||||
|
*
|
||||||
|
* UNE SEULE ACTION PAR PANNE
|
||||||
|
* Après avoir agi, il se verrouille jusqu'au retour d'un signe de vie. Sans ce
|
||||||
|
* verrou, une simple coupure réseau entre les deux NAS ferait rejouer l'action
|
||||||
|
* à chaque cycle pendant que le contrôleur, bien vivant, la défait — un
|
||||||
|
* oscillateur de plus. Une écriture, puis silence.
|
||||||
|
*
|
||||||
|
* Usage :
|
||||||
|
* node tools/watchdog-evcs.mjs (boucle, mode réel)
|
||||||
|
* node tools/watchdog-evcs.mjs --once (un seul cycle)
|
||||||
|
* node tools/watchdog-evcs.mjs --dry-run (n'écrit jamais sur le GX)
|
||||||
|
* node tools/watchdog-evcs.mjs --once --dry-run --simulate-dead
|
||||||
|
* (rejoue une panne, sans risque)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import mqtt from 'mqtt';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
||||||
|
|
||||||
|
const ARGS = new Set(process.argv.slice(2));
|
||||||
|
const ONCE = ARGS.has('--once');
|
||||||
|
const DRY = ARGS.has('--dry-run');
|
||||||
|
const SIMULATE_DEAD = ARGS.has('--simulate-dead');
|
||||||
|
|
||||||
|
const CFG = {
|
||||||
|
n8nUrl: process.env.N8N_URL || 'https://n8n.airtria.pro',
|
||||||
|
workflowId: process.env.EVCS_WORKFLOW_ID || 'ATswvGfG4KJUls0K',
|
||||||
|
gxHost: process.env.GX_MQTT_HOST || '10.10.10.9',
|
||||||
|
gxPort: Number(process.env.GX_MQTT_PORT || 1883),
|
||||||
|
portalId: process.env.GX_PORTAL_ID || 'c0619ab34ac5',
|
||||||
|
evChargerInstance: process.env.GX_EVCHARGER_INSTANCE || '40',
|
||||||
|
|
||||||
|
// Un contrôleur vivant s'exécute toutes les 30 s. Dix minutes = vingt cycles
|
||||||
|
// manqués : assez pour ne pas réagir à un hoquet, assez court pour qu'une
|
||||||
|
// panne nocturne ne coûte pas une journée de production.
|
||||||
|
livenessMaxAgeSec: Number(process.env.WATCHDOG_MAX_AGE_SEC || 600),
|
||||||
|
periodSec: Number(process.env.WATCHDOG_PERIOD_SEC || 60),
|
||||||
|
|
||||||
|
// Valeur de repos de la consigne réseau : le défaut Victron, pas un choix.
|
||||||
|
idleSetpointW: Number(process.env.WATCHDOG_IDLE_SETPOINT_W || 50),
|
||||||
|
// Au-delà de quoi la consigne est jugée « laissée haute ». Un pas de courant
|
||||||
|
// EVCS à 230 V, la granularité à laquelle le contrôleur déplace la puissance.
|
||||||
|
setpointSlackW: Number(process.env.WATCHDOG_SETPOINT_SLACK_W || 230),
|
||||||
|
|
||||||
|
logFile: process.env.WATCHDOG_LOG || path.join(ROOT, 'logs', 'watchdog-evcs.log'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- journal
|
||||||
|
function log(level, msg) {
|
||||||
|
const line = `${new Date().toISOString()} ${level.padEnd(5)} ${msg}`;
|
||||||
|
console.log(line);
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(CFG.logFile), { recursive: true });
|
||||||
|
// Rotation naïve : le chien de garde ne doit jamais remplir le disque
|
||||||
|
// qu'il est censé protéger.
|
||||||
|
try {
|
||||||
|
if (fs.statSync(CFG.logFile).size > 5e6) {
|
||||||
|
fs.renameSync(CFG.logFile, CFG.logFile + '.1');
|
||||||
|
}
|
||||||
|
} catch { /* pas encore de fichier */ }
|
||||||
|
fs.appendFileSync(CFG.logFile, line + '\n');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('journal indisponible :', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- clé n8n
|
||||||
|
function loadApiKey() {
|
||||||
|
if (process.env.N8N_API_KEY) return process.env.N8N_API_KEY;
|
||||||
|
try {
|
||||||
|
const cfg = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(os.homedir(), '.config/opencode/opencode.json'), 'utf8')
|
||||||
|
);
|
||||||
|
const key = cfg?.mcp?.['n8n-mcp']?.environment?.N8N_API_KEY;
|
||||||
|
if (key) return key;
|
||||||
|
} catch { /* absent */ }
|
||||||
|
// Repli pour un déploiement hors poste de travail (conteneur sur 10.10.10.22) :
|
||||||
|
// le .env du dépôt, exclu de git comme le jeton HA.
|
||||||
|
try {
|
||||||
|
const envPath = path.join(ROOT, '.env');
|
||||||
|
const m = fs.readFileSync(envPath, 'utf8').match(/^\s*N8N_API_KEY\s*=\s*(.+?)\s*$/m);
|
||||||
|
if (m) return m[1].replace(/^["']|["']$/g, '');
|
||||||
|
} catch { /* absent */ }
|
||||||
|
log('FATAL', 'Clé API n8n introuvable (N8N_API_KEY ou opencode.json).');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const API_KEY = loadApiKey();
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- signal de vie
|
||||||
|
/**
|
||||||
|
* Renvoie { alive, ageSec, detail }. Toute panne de lecture vaut « mort » :
|
||||||
|
* si le chien de garde ne peut pas joindre n8n, c'est déjà une anomalie, et
|
||||||
|
* l'action qu'il déclenche est de toute façon un retour à l'état sûr.
|
||||||
|
*/
|
||||||
|
async function controllerLiveness() {
|
||||||
|
if (SIMULATE_DEAD) return { alive: false, ageSec: null, detail: 'panne simulée (--simulate-dead)' };
|
||||||
|
try {
|
||||||
|
const ctl = AbortSignal.timeout(10000);
|
||||||
|
const r = await fetch(
|
||||||
|
`${CFG.n8nUrl}/api/v1/executions?workflowId=${encodeURIComponent(CFG.workflowId)}&limit=3`,
|
||||||
|
{ headers: { 'X-N8N-API-KEY': API_KEY }, signal: ctl }
|
||||||
|
);
|
||||||
|
if (!r.ok) return { alive: false, ageSec: null, detail: `n8n HTTP ${r.status}` };
|
||||||
|
const runs = (await r.json()).data || [];
|
||||||
|
if (!runs.length) return { alive: false, ageSec: null, detail: 'aucune exécution connue' };
|
||||||
|
|
||||||
|
const ageSec = (Date.now() - Date.parse(runs[0].startedAt)) / 1000;
|
||||||
|
if (!(ageSec <= CFG.livenessMaxAgeSec)) {
|
||||||
|
return { alive: false, ageSec, detail: `dernière exécution il y a ${Math.round(ageSec)}s` };
|
||||||
|
}
|
||||||
|
// Le contrôleur s'exécute mais échoue à chaque fois : il tourne à vide
|
||||||
|
// (typiquement Home Assistant inaccessible). Trois de suite pour ne pas
|
||||||
|
// réagir à une erreur isolée.
|
||||||
|
if (runs.length >= 3 && runs.every(e => e.status === 'error')) {
|
||||||
|
return { alive: false, ageSec, detail: '3 dernières exécutions en erreur' };
|
||||||
|
}
|
||||||
|
return { alive: true, ageSec, detail: `exécution il y a ${Math.round(ageSec)}s` };
|
||||||
|
} catch (e) {
|
||||||
|
return { alive: false, ageSec: null, detail: `n8n injoignable (${e.message})` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------- GX MQTT
|
||||||
|
const T = {
|
||||||
|
setpoint: `settings/0/Settings/CGwacs/AcPowerSetPoint`,
|
||||||
|
evStartStop: `evcharger/${CFG.evChargerInstance}/StartStop`,
|
||||||
|
evPower: `evcharger/${CFG.evChargerInstance}/Ac/Power`,
|
||||||
|
soc: `battery/512/Soc`,
|
||||||
|
gridPower: `system/0/Ac/Grid/L1/Power`,
|
||||||
|
};
|
||||||
|
const READ = (p) => `N/${CFG.portalId}/${p}`;
|
||||||
|
const WRITE = (p) => `W/${CFG.portalId}/${p}`;
|
||||||
|
|
||||||
|
const gx = { client: null, cache: new Map(), connected: false };
|
||||||
|
|
||||||
|
function gxConnect() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const c = mqtt.connect(`mqtt://${CFG.gxHost}:${CFG.gxPort}`, {
|
||||||
|
connectTimeout: 8000,
|
||||||
|
reconnectPeriod: 5000,
|
||||||
|
clientId: `evcs-watchdog-${Math.random().toString(16).slice(2, 8)}`,
|
||||||
|
});
|
||||||
|
c.on('connect', () => {
|
||||||
|
gx.connected = true;
|
||||||
|
for (const p of Object.values(T)) c.subscribe(READ(p));
|
||||||
|
// Venus ne publie que si on l'entretient : keepalive obligatoire.
|
||||||
|
c.publish(`R/${CFG.portalId}/keepalive`, '');
|
||||||
|
log('INFO', `GX ${CFG.gxHost}:${CFG.gxPort} — connecté`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
c.on('reconnect', () => { gx.connected = false; });
|
||||||
|
c.on('close', () => { gx.connected = false; });
|
||||||
|
c.on('error', (e) => log('WARN', `MQTT: ${e.message}`));
|
||||||
|
c.on('message', (t, m) => {
|
||||||
|
try {
|
||||||
|
const v = JSON.parse(m.toString());
|
||||||
|
if (v && typeof v === 'object' && 'value' in v) gx.cache.set(t, v.value);
|
||||||
|
} catch { /* charge utile inattendue : ignorée */ }
|
||||||
|
});
|
||||||
|
gx.client = c;
|
||||||
|
setTimeout(resolve, 9000); // ne jamais bloquer la boucle sur le broker
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function gxKeepalive() {
|
||||||
|
if (gx.client && gx.connected) gx.client.publish(`R/${CFG.portalId}/keepalive`, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const gxRead = (p) => gx.cache.get(READ(p));
|
||||||
|
|
||||||
|
function gxWrite(p, value) {
|
||||||
|
if (DRY) { log('DRY', `écriture SUPPRIMÉE ${WRITE(p)} = ${value}`); return; }
|
||||||
|
if (!gx.client || !gx.connected) { log('ERROR', `GX injoignable : ${p} = ${value} NON écrit`); return; }
|
||||||
|
gx.client.publish(WRITE(p), JSON.stringify({ value }));
|
||||||
|
log('ACT', `${WRITE(p)} = ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------- boucle
|
||||||
|
let armed = true; // faux = action déjà faite, on attend un signe de vie
|
||||||
|
let lastAliveLog = 0;
|
||||||
|
|
||||||
|
async function cycle() {
|
||||||
|
gxKeepalive();
|
||||||
|
const live = await controllerLiveness();
|
||||||
|
|
||||||
|
if (live.alive) {
|
||||||
|
if (!armed) log('INFO', `contrôleur revenu (${live.detail}) — chien de garde réarmé`);
|
||||||
|
armed = true;
|
||||||
|
// Trace discrète : une ligne par heure suffit à prouver qu'il veille.
|
||||||
|
if (Date.now() - lastAliveLog > 3600e3) {
|
||||||
|
lastAliveLog = Date.now();
|
||||||
|
log('INFO', `veille — contrôleur vivant (${live.detail}), consigne ${gxRead(T.setpoint) ?? '?'} W`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setpointW = gxRead(T.setpoint);
|
||||||
|
const evPowerW = gxRead(T.evPower);
|
||||||
|
const evOn = gxRead(T.evStartStop);
|
||||||
|
const dangerousSetpoint =
|
||||||
|
Number.isFinite(setpointW) && setpointW > CFG.idleSetpointW + CFG.setpointSlackW;
|
||||||
|
const evActive = evOn === 1 || (Number.isFinite(evPowerW) && evPowerW > 100);
|
||||||
|
|
||||||
|
if (!dangerousSetpoint && !evActive) {
|
||||||
|
log('WARN', `contrôleur muet (${live.detail}) — mais rien à reprendre ` +
|
||||||
|
`(consigne ${setpointW ?? '?'} W, borne ${evActive ? 'active' : 'arrêtée'})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!armed) return; // déjà agi pour cette panne : on se tait
|
||||||
|
|
||||||
|
log('ALERT', `CONTRÔLEUR MUET — ${live.detail}. ` +
|
||||||
|
`Consigne ${setpointW ?? '?'} W, borne ${evPowerW ?? '?'} W, SOC ${gxRead(T.soc) ?? '?'}%, ` +
|
||||||
|
`réseau ${gxRead(T.gridPower) ?? '?'} W. Retour à l'état sûr.`);
|
||||||
|
|
||||||
|
if (dangerousSetpoint) gxWrite(T.setpoint, CFG.idleSetpointW);
|
||||||
|
if (evActive) gxWrite(T.evStartStop, 0);
|
||||||
|
|
||||||
|
armed = false;
|
||||||
|
log('ALERT', 'Action unique effectuée. Aucune autre écriture avant le retour du contrôleur. ' +
|
||||||
|
'Le contacteur EDF n\'est PAS touché (Tuya cloud, et consigne au repos = état sûr).');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------- main
|
||||||
|
log('INFO', `démarrage — n8n ${CFG.n8nUrl} (workflow ${CFG.workflowId}), GX ${CFG.gxHost}, ` +
|
||||||
|
`seuil ${CFG.livenessMaxAgeSec}s, période ${CFG.periodSec}s` +
|
||||||
|
`${DRY ? ' [DRY-RUN]' : ''}${SIMULATE_DEAD ? ' [PANNE SIMULÉE]' : ''}`);
|
||||||
|
|
||||||
|
await gxConnect();
|
||||||
|
|
||||||
|
if (ONCE) {
|
||||||
|
await new Promise(r => setTimeout(r, 3000)); // laisser le cache se remplir
|
||||||
|
await cycle();
|
||||||
|
gx.client?.end();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 3000));
|
||||||
|
await cycle();
|
||||||
|
setInterval(() => { cycle().catch(e => log('ERROR', `cycle: ${e.message}`)); }, CFG.periodSec * 1000);
|
||||||
|
|
||||||
|
for (const sig of ['SIGINT', 'SIGTERM']) {
|
||||||
|
process.on(sig, () => { log('INFO', `arrêt (${sig})`); gx.client?.end(); process.exit(0); });
|
||||||
|
}
|
||||||
90
tools/watchdog-selftest.mjs
Normal file
90
tools/watchdog-selftest.mjs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Preuve de bout en bout du CHEMIN D'ÉCRITURE du chien de garde.
|
||||||
|
*
|
||||||
|
* Le `--dry-run` valide la détection, le verrou et le journal, mais il
|
||||||
|
* supprime précisément ce qui compte le jour d'une panne : la publication
|
||||||
|
* MQTT vers le GX. Si le broker refusait les topics `W/`, le chien de garde
|
||||||
|
* serait décoratif — et on ne l'apprendrait qu'au pire moment. Ce test écrit
|
||||||
|
* réellement, relit, puis REMET la valeur d'origine lui-même.
|
||||||
|
*
|
||||||
|
* Sûreté : il n'écrit que sur la consigne de puissance réseau, il mémorise la
|
||||||
|
* valeur trouvée avant de la modifier et la restaure quoi qu'il arrive
|
||||||
|
* (y compris sur erreur ou Ctrl-C). La valeur sonde est volontairement
|
||||||
|
* absurde (777 W) pour être reconnaissable dans un journal.
|
||||||
|
*
|
||||||
|
* Usage : node tools/watchdog-selftest.mjs
|
||||||
|
*/
|
||||||
|
import mqtt from 'mqtt';
|
||||||
|
|
||||||
|
const HOST = process.env.GX_MQTT_HOST || '10.10.10.9';
|
||||||
|
const PORT = Number(process.env.GX_MQTT_PORT || 1883);
|
||||||
|
const P = process.env.GX_PORTAL_ID || 'c0619ab34ac5';
|
||||||
|
const EV = process.env.GX_EVCHARGER_INSTANCE || '40';
|
||||||
|
const PROBE_W = 777;
|
||||||
|
|
||||||
|
const SETPOINT = 'settings/0/Settings/CGwacs/AcPowerSetPoint';
|
||||||
|
const STARTSTOP = `evcharger/${EV}/StartStop`;
|
||||||
|
const ACIN = 'system/0/Ac/Grid/L1/Power';
|
||||||
|
|
||||||
|
const cache = new Map();
|
||||||
|
const c = mqtt.connect(`mqtt://${HOST}:${PORT}`, { connectTimeout: 8000 });
|
||||||
|
const read = (p) => cache.get(`N/${P}/${p}`);
|
||||||
|
const write = (p, value) => c.publish(`W/${P}/${p}`, JSON.stringify({ value }));
|
||||||
|
const wait = (ms) => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
|
let original = null;
|
||||||
|
async function restore(why) {
|
||||||
|
if (original === null) return;
|
||||||
|
write(SETPOINT, original);
|
||||||
|
await wait(1500);
|
||||||
|
console.log(` restauration (${why}) → consigne ${read(SETPOINT)} W`);
|
||||||
|
}
|
||||||
|
process.on('SIGINT', async () => { await restore('interruption'); process.exit(1); });
|
||||||
|
|
||||||
|
c.on('connect', async () => {
|
||||||
|
for (const t of [SETPOINT, STARTSTOP, ACIN]) c.subscribe(`N/${P}/${t}`);
|
||||||
|
c.publish(`R/${P}/keepalive`, '');
|
||||||
|
await wait(2500);
|
||||||
|
|
||||||
|
original = read(SETPOINT);
|
||||||
|
console.log(`GX ${HOST}:${PORT} — connecté`);
|
||||||
|
console.log(`état trouvé : consigne ${original} W · borne StartStop=${read(STARTSTOP)} · réseau ${read(ACIN)} W\n`);
|
||||||
|
|
||||||
|
if (typeof original !== 'number') {
|
||||||
|
console.log('❌ consigne illisible : le test ne peut pas garantir la restauration. ABANDON.');
|
||||||
|
c.end(); process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`1) écriture réelle de la sonde : ${PROBE_W} W`);
|
||||||
|
write(SETPOINT, PROBE_W);
|
||||||
|
await wait(3000);
|
||||||
|
const seen = read(SETPOINT);
|
||||||
|
console.log(` relecture : ${seen} W`);
|
||||||
|
const ok = seen === PROBE_W;
|
||||||
|
console.log(ok
|
||||||
|
? ' ✅ le GX accepte les écritures du chien de garde\n'
|
||||||
|
: ` ❌ écriture SANS EFFET (attendu ${PROBE_W}) — le chien de garde serait décoratif\n`);
|
||||||
|
|
||||||
|
console.log(`2) remise de la valeur d'origine : ${original} W`);
|
||||||
|
write(SETPOINT, original);
|
||||||
|
await wait(3000);
|
||||||
|
const back = read(SETPOINT);
|
||||||
|
console.log(` relecture : ${back} W ${back === original ? '✅' : '❌'}\n`);
|
||||||
|
|
||||||
|
console.log(ok && back === original
|
||||||
|
? 'VERDICT : chemin d\'écriture DÉMONTRÉ, état d\'origine rétabli.'
|
||||||
|
: 'VERDICT : ANOMALIE — voir ci-dessus.');
|
||||||
|
process.exitCode = ok && back === original ? 0 : 1;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('erreur :', e.message);
|
||||||
|
await restore('erreur');
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
c.end();
|
||||||
|
setTimeout(() => process.exit(process.exitCode ?? 0), 500);
|
||||||
|
});
|
||||||
|
c.on('message', (t, m) => { try { const v = JSON.parse(m.toString()); if (v && 'value' in v) cache.set(t, v.value); } catch {} });
|
||||||
|
c.on('error', (e) => { console.error('MQTT:', e.message); process.exit(1); });
|
||||||
|
setTimeout(() => { console.error('délai dépassé'); process.exit(1); }, 30000);
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user