Compare commits
2 Commits
c9dd79c30f
...
057e0f7d8f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
057e0f7d8f | ||
|
|
522f47e821 |
96
HANDOFF.md
96
HANDOFF.md
@@ -1242,3 +1242,99 @@ branché mi-session)
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Document mis à jour le : 22/08/2026 (~18:05) — Fix G déployé et validé (syntaxe + golden-hysteresis 7/7 + PUT OK), à confirmer en conditions réelles à la prochaine fermeture de fenêtre HC mi-session
|
*Document mis à jour le : 22/08/2026 (~18:05) — Fix G déployé et validé (syntaxe + golden-hysteresis 7/7 + PUT OK), à confirmer en conditions réelles à la prochaine fermeture de fenêtre HC mi-session
|
||||||
|
|
||||||
|
## 25. Session du 24/08/2026 — Fix G retiré + diagnostic des deux oscillateurs
|
||||||
|
|
||||||
|
### Fix G retiré de la production (déployé 24/08 12:18, golden-hysteresis 7/7)
|
||||||
|
Fix G (§24) a produit en production un yoyo EDF de 2-4 min la nuit du 23-24/08,
|
||||||
|
véhicule débranché : coupure à SOC 21 % (Fix G, bypass min_on) puis rallumage à
|
||||||
|
SOC 20 % (branche `hold_on_low_soc`, bypass cooldown), MPPT qui flappe à chaque
|
||||||
|
bascule. Logbook : `turn_off_evcs_override_released` 06:43:03 → `hold_on_low_soc`
|
||||||
|
06:45:03 → `turn_on_low_soc_cooldown_bypassed` 06:52:03.
|
||||||
|
|
||||||
|
**Erreur de conception** : le garde `soc > gridSocOn` ne protégeait que l'INSTANT
|
||||||
|
de l'engagement sur SOC bas (`soc <= 20`), pas la PHASE DE REMONTÉE 20 → 30 qui
|
||||||
|
est 100 % de la durée de vie de ce cas. Fix G a donc remplacé l'hystérésis 20/30
|
||||||
|
par une hystérésis 20/20,1.
|
||||||
|
|
||||||
|
**Erreur de test** : le fixture `tests/golden-hysteresis/` portait `grid_soc_on:
|
||||||
|
"22"` / `grid_soc_off: "35"` — les *valeurs de repli du code*, pas la production
|
||||||
|
(vérifiée via MCP : **20 / 30**, inchangées depuis le 07/08). Et le scénario de
|
||||||
|
non-régression `g-baseline-low-soc-no-override` utilisait SOC 18 %, c'est-à-dire
|
||||||
|
le seul point que Fix G épargnait. → **Règle : figer un fixture de test sur des
|
||||||
|
valeurs lues en live via MCP, jamais sur les fallbacks du code.**
|
||||||
|
|
||||||
|
Retrait : `tools/patch-20260824-revert-g.mjs` (rejouable) — le jsCode résultant
|
||||||
|
est byte-identique à la version pré-Fix-G (commit `0419c74`), vérifié.
|
||||||
|
Suite réécrite (7 scénarios, seuils réels 20/30) avec les garde-fous manquants
|
||||||
|
`h-recovery-band-no-cut-21` / `-25` : la remontée batterie sur réseau ne doit
|
||||||
|
JAMAIS être interrompue avant `grid_soc_off`. Test de mutation : Fix G réappliqué
|
||||||
|
→ 4/7 échecs, retiré → 7/7. Le filet attrape désormais la régression.
|
||||||
|
|
||||||
|
### Diagnostic : deux oscillateurs, même pathologie
|
||||||
|
Détail complet dans `docs/MANUEL_COMPLET.md` §11.8. Résumé :
|
||||||
|
|
||||||
|
**A — Yoyo EDF.** Le problème de fond n'est pas le réglage de `grid_soc_off`
|
||||||
|
mais le fait qu'il n'a aucun sens physique : le 24/08, la PV ne couvre la conso
|
||||||
|
maison qu'à partir de ~09:00 (0,07 → 0,63 → 2,74 kWh/h contre 0,75 kWh/h de
|
||||||
|
conso). Couper EDF à 06:43 à 21 % rend le yoyo inévitable quels que soient les
|
||||||
|
seuils. Le workflow hystérésis lit 25 champs et **aucune prévision solaire**.
|
||||||
|
Cible : session réseau unique dimensionnée jusqu'au croisement PV/conso,
|
||||||
|
arbitrée par le conseiller IA. NB : mesurer la vraie capacité PV impose de
|
||||||
|
couper EDF (sinon le MPPT est bridé et on ne voit rien).
|
||||||
|
|
||||||
|
**B — Oscillation charge EVCS en plein soleil** (24/08 12:25 → 12:28).
|
||||||
|
Ce n'est PAS une limite 5 kVA (aucun `ve_bus_overload` depuis le 20/08,
|
||||||
|
`ac_loads_max_safe_w` = 9000, AC-out L1 à 5335 W sans overload, plafond calculé
|
||||||
|
~20 A) et **pas non plus une bande morte batterie trop étroite** : le courant
|
||||||
|
batterie était à **+127 A** au moment de l'arrêt. Les deux atténuations que
|
||||||
|
j'avais d'abord proposées (`battery_discharge_soft_a` à −12 A,
|
||||||
|
`battery_neutral_band_a` à 3-5 A) n'avaient donc **aucun effet sur ce cas** —
|
||||||
|
l'utilisateur l'a signalé, à raison, avant que je les applique.
|
||||||
|
|
||||||
|
Cause réelle : **inversion du plancher de démarrage et du plancher de maintien**.
|
||||||
|
À 12:24:00 le profil bascule vers `midday_hc_or_solar` ; avec SOC 58 % le
|
||||||
|
plancher de démarrage vaut `min(midday_soc_min 75, abondance P6 40)` = **40**
|
||||||
|
et le plancher de maintien `max(soc_stop_min 29, 75-15)` = **60**. Le système a
|
||||||
|
le droit de DÉMARRER ce qu'il n'a pas le droit de MAINTENIR → cycle limite à la
|
||||||
|
période de `min_run_minutes` (3 min) sur toute la bande [40 %, 60 %], et ~2,6 kW
|
||||||
|
de PV écrêtés à chaque creux (PV 10,24 kW → 7,64 kW quand la borne s'arrête).
|
||||||
|
|
||||||
|
### Décisions de conception validées avec l'utilisateur
|
||||||
|
- La conso se **lit**, elle ne se devine pas ; filtrage numérique dynamique des
|
||||||
|
pointes (improbables la nuit hors charge EVCS ; pas de moyenne quand aucun
|
||||||
|
véhicule n'est branché).
|
||||||
|
- SOC : lissage pour les décisions de confort, **valeur instantanée conservée
|
||||||
|
pour la sécurité** (le risque prime).
|
||||||
|
- Les seuils fixes doivent devenir dynamiques (saison, prévision) et être
|
||||||
|
arbitrés par le conseiller IA : « un automatisme c'est trop rigide pour
|
||||||
|
traiter tous ces cas tordus ». Ne plus empiler de branches codées en dur.
|
||||||
|
- Exploiter les messages d'état réels du Victron et de la borne au lieu de
|
||||||
|
s'auto-limiter à l'aveugle sur un proxy.
|
||||||
|
|
||||||
|
---
|
||||||
|
### Correctif déployé 24/08 12:45 — invariant maintien <= démarrage
|
||||||
|
`src/nodes/BATTERY_CLOUD_LOCKS.js`, deux volets indissociables :
|
||||||
|
1. `effectiveSocHoldFloor = min(règle du profil, plancher de démarrage effectif)`
|
||||||
|
— on ne peut plus être autorisé à démarrer ce qu'on n'est pas autorisé à
|
||||||
|
maintenir. Sans abondance, comportement inchangé.
|
||||||
|
2. L'abondance P6 se PROUVE au démarrage (`solarAbundanceStart` : pvAvg >= 4000
|
||||||
|
ET batterie >= +20 A) mais se MAINTIENT en charge (`solarAbundanceHold` :
|
||||||
|
pvAvg >= 4000 ET batterie >= 0). Sans ce dédoublement, la charge du véhicule
|
||||||
|
détourne le surplus, fait tomber le courant batterie sous +20 A et détruit sa
|
||||||
|
propre condition d'autorisation — l'oscillation reviendrait dès que le PV
|
||||||
|
n'est que moyennement fort.
|
||||||
|
|
||||||
|
Golden 39 → 40 : `midday-inversion-start-hold` (relevé VRM du 24/08 12:25 :
|
||||||
|
PV 10,24 kW, charges 3815 W dont EVCS 3502 W, batterie +127 A, SOC 58 %).
|
||||||
|
Preuve par mutation : code d'avant → `stop` / « Conditions de maintien perdues » ;
|
||||||
|
après → `charging_step_up_battery` à 19 A. Les 39 scénarios existants inchangés.
|
||||||
|
|
||||||
|
**Leçon transverse** : trois oscillateurs identifiés le même jour (Fix G,
|
||||||
|
abondance P6 auto-destructrice, inversion démarrage/maintien) — même pathologie
|
||||||
|
à chaque fois, des seuils empilés par couches successives, chacun raisonnable
|
||||||
|
isolément, dont personne ne vérifie la cohérence d'ensemble. D'où le chantier
|
||||||
|
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*
|
||||||
|
|||||||
@@ -445,13 +445,7 @@ Y1 EDF ON + latch
|
|||||||
│ (turn_off_vehicle_unplugged, bypass min_on — Fix C)
|
│ (turn_off_vehicle_unplugged, bypass min_on — Fix C)
|
||||||
│ v4: es ∈ {night_recovery_done_idle, departure_done_idle} + SOC ≥ seuil OFF
|
│ v4: es ∈ {night_recovery_done_idle, departure_done_idle} + SOC ≥ seuil OFF
|
||||||
│ → coupure immédiate (turn_off_recovery_done, bypass min_on — E2+P9)
|
│ → coupure immédiate (turn_off_recovery_done, bypass min_on — E2+P9)
|
||||||
│ v5: override EVCS disparu (evcsGridOverride → false) ET SOC > seuil ON
|
│ (v5 "Fix G" a existé du 22 au 24/08/2026 puis a été RETIRÉ : voir §11.8)
|
||||||
│ (PAS seuil OFF : ne touche pas au cas indépendant "SOC bas")
|
|
||||||
│ → coupure immédiate (turn_off_evcs_override_released, bypass
|
|
||||||
│ min_on — Fix G, 22/08/2026). Contrairement à v3/v4, ne dépend PAS
|
|
||||||
│ d'un nom exact de `surplus_state` — couvre tout état transitoire
|
|
||||||
│ (y compris ceux non encore nommés). Garde anti-glitch : ignoré si
|
|
||||||
│ la lecture `evcs_source_mode` est vide/unavailable/unknown.
|
|
||||||
└─▶ retour Y0 (cooldown 15 min)
|
└─▶ retour Y0 (cooldown 15 min)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -488,8 +482,12 @@ retesté. Remises à zéro : débranchement, session fraîche (P11), ou force_*.
|
|||||||
terminé) → `surplus_state` retombe sur un état générique transitoire
|
terminé) → `surplus_state` retombe sur un état générique transitoire
|
||||||
(`stop_source_lost`/`idle_wait_source`) qu'aucune règle v3/v4 ne reconnaît →
|
(`stop_source_lost`/`idle_wait_source`) qu'aucune règle v3/v4 ne reconnaît →
|
||||||
EDF maintenu des heures par la boucle générique jusqu'à `gridSocOff`, PV
|
EDF maintenu des heures par la boucle générique jusqu'à `gridSocOff`, PV
|
||||||
plafonné ~100 W par le MPPT. → Fix G (v5 : coupure générique dès que
|
plafonné ~100 W par le MPPT. → Fix G (v5), **retiré le 24/08** : mauvais
|
||||||
l'override EVCS disparaît, sans dépendre du nom d'état `surplus_state`).
|
diagnostic, le sujet réel est le bilan énergétique (§11.8).
|
||||||
|
- **Nuit 23-24/08 (yoyo EDF de 2-4 min)** : Fix G coupait à SOC 21 % ce que la
|
||||||
|
branche « SOC bas » venait de rallumer à 20 % → hystérésis 20/30 réduite à
|
||||||
|
20/20,1, MPPT qui flappe à chaque bascule. → Fix G retiré, garde-fous
|
||||||
|
anti-yoyo ajoutés à `tests/golden-hysteresis/` (§11.8).
|
||||||
|
|
||||||
### 11.7 Conseiller IA — mode ombre et câblages réels
|
### 11.7 Conseiller IA — mode ombre et câblages réels
|
||||||
|
|
||||||
@@ -505,6 +503,93 @@ retesté. Remises à zéro : débranchement, session fraîche (P11), ou force_*.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 11.8 Les deux oscillateurs identifiés le 24/08/2026
|
||||||
|
|
||||||
|
Deux boucles de rétroaction distinctes, de même nature : **une décision prise
|
||||||
|
sur un seuil que l'action elle-même vient détruire**, sans bande morte.
|
||||||
|
|
||||||
|
#### A. Yoyo EDF (workflow hystérésis) — Fix G retiré
|
||||||
|
|
||||||
|
Fix G coupait EDF dès que l'override EVCS disparaissait et que
|
||||||
|
`soc > grid_soc_on`. Or la branche « SOC bas » allume EDF à `grid_soc_on`
|
||||||
|
(20 %) et doit tenir jusqu'à `grid_soc_off` (30 %) : pendant TOUTE cette
|
||||||
|
remontée, `soc > grid_soc_on` est vrai. Fix G coupait donc à 21 % ce que la
|
||||||
|
branche basse rallumait à 20 %, les deux en bypass de leur amortisseur
|
||||||
|
(min_on 25 min d'un côté, cooldown 15 min de l'autre) → oscillateur libre de
|
||||||
|
2 à 4 minutes, MPPT qui flappe à chaque bascule. Retiré le 24/08.
|
||||||
|
|
||||||
|
**Le problème de fond n'est pas le seuil mais son absence de sens physique.**
|
||||||
|
Le 24/08, conso maison ≈ 0,75 kWh/h de 07:00 à 09:00 pour une production
|
||||||
|
solaire de 0,07 puis 0,63 kWh/h : rien ne peut relayer EDF avant ~09:00, où la
|
||||||
|
PV passe à 2,74 kWh/h et la batterie remonte seule de 21 à 31 %. Couper EDF à
|
||||||
|
06:43 à 21 % rend le yoyo **mécaniquement inévitable, quels que soient les
|
||||||
|
seuils**. Double peine : EDF ON bride le MPPT (ESS sans injection), donc la
|
||||||
|
seule ressource capable de sortir du yoyo est étouffée par le yoyo.
|
||||||
|
|
||||||
|
Constat structurel : le snapshot du workflow hystérésis lit 25 champs et
|
||||||
|
**aucune prévision solaire, aucune conso projetée, aucun helper advisor**. Il
|
||||||
|
décide sur un point de SOC instantané comparé à deux nombres fixes. `30 %`
|
||||||
|
n'est pas mal réglé, il est hors sujet : la bonne valeur dépend de l'heure, de
|
||||||
|
la saison et de la courbe du jour (plus haute en saison des pluies, plus basse
|
||||||
|
par forte prévision). Cible retenue : une **session réseau unique et
|
||||||
|
dimensionnée** (charger en une fois jusqu'au niveau qui permet de tenir
|
||||||
|
jusqu'au croisement PV/conso), arbitrée par le conseiller IA, les règles codées
|
||||||
|
restant un filet de sécurité borné.
|
||||||
|
|
||||||
|
#### B. Oscillation de la charge EVCS en plein soleil
|
||||||
|
|
||||||
|
Observée le 24/08 entre 12:25 et 12:28 : la borne charge à 18 A / 3502 W avec
|
||||||
|
PV 10,24 kW et batterie à **+127 A**, puis s'arrête (0 A, `Waiting for start`)
|
||||||
|
— et la PV retombe à 7638 W, soit **~2,6 kW écrêtés** faute de preneur (ESS
|
||||||
|
sans injection, batterie déjà à son courant de charge maximal).
|
||||||
|
|
||||||
|
**Ce n'est pas une limite 5 kVA** : aucun `binary_sensor.victron_ve_bus_overload`
|
||||||
|
depuis le 20/08, `ac_loads_max_safe_w` = 9000 W, AC-out L1 mesuré à 5335 W sans
|
||||||
|
overload (le parallèle maître/esclave tient bien au-delà de 5 kVA), plafond
|
||||||
|
calculé par `WINDOWS_SOURCEV2` ~20 A. **Ce n'est pas non plus une bande morte
|
||||||
|
batterie trop étroite** : le courant était à +127 A au moment de l'arrêt, très
|
||||||
|
loin de tout seuil de décharge.
|
||||||
|
|
||||||
|
**Cause réelle : inversion du plancher de démarrage et du plancher de maintien.**
|
||||||
|
À 12:24:00 le profil bascule `outside_window` → `midday_hc_or_solar`. Dans ce
|
||||||
|
profil, avec SOC 58 % :
|
||||||
|
|
||||||
|
| | Calcul | Valeur |
|
||||||
|
|---|---|---|
|
||||||
|
| Plancher de **démarrage** | `min(midday_soc_min 75, abondance P6 40)` | **40 %** |
|
||||||
|
| Plancher de **maintien** | `max(soc_stop_min 29, midday_soc_min 75 − 15)` | **60 %** |
|
||||||
|
|
||||||
|
`40 ≤ 58 < 60` : le système a le droit de **démarrer** ce qu'il n'a pas le
|
||||||
|
droit de **maintenir**. Cycle limite garanti à la période de `min_run_minutes`
|
||||||
|
(3 min) sur toute la bande [40 %, 60 %], traversée chaque jour vers midi.
|
||||||
|
L'origine est P6, qui abaissait le plancher de démarrage en cas d'abondance
|
||||||
|
sans toucher au plancher de maintien.
|
||||||
|
|
||||||
|
**Correctif déployé le 24/08 12:45** (`BATTERY_CLOUD_LOCKS.js`), deux volets :
|
||||||
|
|
||||||
|
1. **Invariant maintien ≤ démarrage** :
|
||||||
|
`effectiveSocHoldFloor = min(règle du profil, plancher de démarrage effectif)`.
|
||||||
|
Sans abondance, rien ne change (60 reste 60).
|
||||||
|
2. **L'abondance se prouve au démarrage, se maintient en charge.**
|
||||||
|
`solarAbundanceStart` exige toujours `pvAvg ≥ 4000 W` **et**
|
||||||
|
`batteryCurrentA ≥ +20 A` (preuve avant d'agir) ; `solarAbundanceHold`
|
||||||
|
n'exige que `pvAvg ≥ 4000 W` **et** `batteryCurrentA ≥ 0` (la batterie n'est
|
||||||
|
pas devenue fournisseuse). Sans cette distinction, la charge du véhicule —
|
||||||
|
qui détourne précisément le surplus — fait tomber le courant batterie sous
|
||||||
|
+20 A et **détruit sa propre condition d'autorisation** : système bistable.
|
||||||
|
|
||||||
|
Scénario golden `midday-inversion-start-hold` (relevé VRM du 24/08 12:25).
|
||||||
|
Preuve par mutation : sur le code d'avant → `stop` / « Conditions de maintien
|
||||||
|
perdues » ; après → `charging_step_up_battery` à 19 A. Golden 39 → 40.
|
||||||
|
|
||||||
|
Direction de fond (même logique qu'en A) : lire la conso au lieu de la
|
||||||
|
deviner, la filtrer numériquement (les pointes nocturnes sont improbables hors
|
||||||
|
charge EVCS), lisser le SOC pour les décisions de confort en gardant
|
||||||
|
l'instantané pour la sécurité, et exploiter les messages d'état réels du
|
||||||
|
Victron et de la borne plutôt que de s'auto-limiter à l'aveugle sur un proxy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 12. Configuration opencode.json
|
## 12. Configuration opencode.json
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -405,12 +405,25 @@ const profileSocStartFloor =
|
|||||||
const PV_ABUNDANCE_AVG_MIN_W = 4000;
|
const PV_ABUNDANCE_AVG_MIN_W = 4000;
|
||||||
const ABUNDANCE_BATTERY_CHARGE_MIN_A = 20;
|
const ABUNDANCE_BATTERY_CHARGE_MIN_A = 20;
|
||||||
const ABUNDANCE_SOC_FLOOR_PCT = 40;
|
const ABUNDANCE_SOC_FLOOR_PCT = 40;
|
||||||
const solarAbundance =
|
|
||||||
|
// 24/08/2026 — L'abondance se PROUVE au démarrage (batterie qui monte franchement,
|
||||||
|
// >= +20 A, avant qu'on touche à quoi que ce soit) mais elle se MAINTIENT tant que
|
||||||
|
// le PV reste fort ET que la batterie n'est pas devenue fournisseuse. Sans cette
|
||||||
|
// distinction, la charge du véhicule — qui détourne précisément le surplus — fait
|
||||||
|
// tomber le courant batterie sous +20 A et détruit sa propre condition
|
||||||
|
// d'autorisation : système bistable, oscillation à la période de min_run.
|
||||||
|
const solarAbundanceStart =
|
||||||
parsed.pvPowerAvg10mW >= PV_ABUNDANCE_AVG_MIN_W &&
|
parsed.pvPowerAvg10mW >= PV_ABUNDANCE_AVG_MIN_W &&
|
||||||
parsed.batteryCurrentA >= ABUNDANCE_BATTERY_CHARGE_MIN_A;
|
parsed.batteryCurrentA >= ABUNDANCE_BATTERY_CHARGE_MIN_A;
|
||||||
const effectiveSocStartFloor = solarAbundance
|
const solarAbundanceHold =
|
||||||
? Math.min(profileSocStartFloor, ABUNDANCE_SOC_FLOOR_PCT)
|
parsed.pvPowerAvg10mW >= PV_ABUNDANCE_AVG_MIN_W &&
|
||||||
: profileSocStartFloor;
|
parsed.batteryCurrentA >= 0;
|
||||||
|
const solarAbundance = solarAbundanceStart; // conservé pour la traçabilité/debug
|
||||||
|
|
||||||
|
const abundanceSocFloor = (abundant) =>
|
||||||
|
abundant ? Math.min(profileSocStartFloor, ABUNDANCE_SOC_FLOOR_PCT) : profileSocStartFloor;
|
||||||
|
|
||||||
|
const effectiveSocStartFloor = abundanceSocFloor(solarAbundanceStart);
|
||||||
|
|
||||||
const profileSocHoldFloor =
|
const profileSocHoldFloor =
|
||||||
context.profile === 'morning_solar' ? Math.max(params.socStopMin, params.morningSocMin - 10) :
|
context.profile === 'morning_solar' ? Math.max(params.socStopMin, params.morningSocMin - 10) :
|
||||||
@@ -418,6 +431,19 @@ const profileSocHoldFloor =
|
|||||||
context.profile === 'evening_high_soc' ? params.eveningSocStopMin :
|
context.profile === 'evening_high_soc' ? params.eveningSocStopMin :
|
||||||
params.socStopMin;
|
params.socStopMin;
|
||||||
|
|
||||||
|
// INVARIANT (24/08/2026) : on ne doit JAMAIS avoir le droit de DÉMARRER une charge
|
||||||
|
// qu'on n'a pas le droit de MAINTENIR. Le plancher de maintien doit donc rester
|
||||||
|
// <= au plancher de démarrage effectif.
|
||||||
|
// Incident du 24/08 midi : bascule de profil vers midday_hc_or_solar à 12:24 avec
|
||||||
|
// SOC 58 % => plancher de démarrage 40 % (abondance P6) mais plancher de maintien
|
||||||
|
// max(29, 75-15) = 60 % => démarrage autorisé, maintien refusé, cycle limite de
|
||||||
|
// 3 min (min_run) sur toute la bande [40 %, 60 %], et ~2,6 kW de PV écrêtés à
|
||||||
|
// chaque creux faute de preneur (ESS sans injection).
|
||||||
|
const effectiveSocHoldFloor = Math.min(
|
||||||
|
profileSocHoldFloor,
|
||||||
|
abundanceSocFloor(solarAbundanceHold)
|
||||||
|
);
|
||||||
|
|
||||||
const profileSpecificDischargeFloorA =
|
const profileSpecificDischargeFloorA =
|
||||||
context.profile === 'morning_solar' ? params.morningBatteryMaxDischargeA :
|
context.profile === 'morning_solar' ? params.morningBatteryMaxDischargeA :
|
||||||
context.profile === 'midday_hc_or_solar' ? params.middayBatteryMaxDischargeA :
|
context.profile === 'midday_hc_or_solar' ? params.middayBatteryMaxDischargeA :
|
||||||
@@ -553,7 +579,7 @@ const solarStartOk =
|
|||||||
const rawSolarHoldOk =
|
const rawSolarHoldOk =
|
||||||
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||||
commonSafetyHoldOk &&
|
commonSafetyHoldOk &&
|
||||||
parsed.soc >= profileSocHoldFloor &&
|
parsed.soc >= effectiveSocHoldFloor &&
|
||||||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
(
|
(
|
||||||
parsed.pvPowerW >= params.pvHoldMinW ||
|
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||||
@@ -862,7 +888,9 @@ return [{
|
|||||||
profileSocStartFloor,
|
profileSocStartFloor,
|
||||||
effectiveSocStartFloor,
|
effectiveSocStartFloor,
|
||||||
solarAbundance,
|
solarAbundance,
|
||||||
|
solarAbundanceHold,
|
||||||
profileSocHoldFloor,
|
profileSocHoldFloor,
|
||||||
|
effectiveSocHoldFloor,
|
||||||
profileBatteryDischargeFloorA,
|
profileBatteryDischargeFloorA,
|
||||||
batteryCurrentHardExceeded,
|
batteryCurrentHardExceeded,
|
||||||
mustStopHard,
|
mustStopHard,
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
"notifications_enabled": "on",
|
"notifications_enabled": "on",
|
||||||
"grid_latch": "off",
|
"grid_latch": "off",
|
||||||
|
|
||||||
"grid_soc_on": "22",
|
"grid_soc_on": "20",
|
||||||
"grid_soc_off": "35",
|
"grid_soc_off": "30",
|
||||||
"grid_min_on_minutes": "25",
|
"grid_min_on_minutes": "25",
|
||||||
"grid_cooldown_minutes": "15",
|
"grid_cooldown_minutes": "15",
|
||||||
"ac_loads_max_safe_w": "7800",
|
"ac_loads_max_safe_w": "7800",
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
// Scénarios golden pour le node "Compute Grid Hysteresis"
|
// Scénarios golden pour le node "Compute Grid Hysteresis"
|
||||||
// (workflow "Victron - Battery / Grid Hysteresis Fix V1").
|
// (workflow "Victron - Battery / Grid Hysteresis Fix V1").
|
||||||
//
|
//
|
||||||
// Contexte (Fix G, 22/08/2026) : incident nuit 21-22/08 — la fenêtre HC nuit
|
// Seuils du fixture = valeurs RÉELLES de production (vérifiées via MCP
|
||||||
// se ferme pendant que le véhicule est encore en session active (pas encore
|
// ha-get-state le 24/08/2026) : grid_soc_on = 20, grid_soc_off = 30,
|
||||||
// flag "charge terminée"). Le contrôleur EVCS relâche correctement son
|
// min_on = 25 min, cooldown = 15 min. Ne PAS utiliser les valeurs de repli
|
||||||
// override (helper night_grid_takeover_needed -> off, source_mode -> Bloqué)
|
// codées dans le node (22/35) : elles ne correspondent à rien en production
|
||||||
// mais émet un état transitoire générique (stop_source_lost / idle_wait_source)
|
// et masquent les bugs de bande.
|
||||||
// qu'aucune règle de coupure immédiate existante (Fix C/E2) ne reconnaît.
|
//
|
||||||
// EDF restait alors maintenu par la boucle générique jusqu'au seuil
|
// Historique : cette suite a été créée le 22/08/2026 pour valider "Fix G"
|
||||||
// grid_soc_off (35%), potentiellement des heures.
|
// (coupure immédiate dès que l'override EVCS disparaissait et que
|
||||||
|
// SOC > grid_soc_on). Fix G a été RETIRÉ le 24/08/2026 : son garde SOC ne
|
||||||
|
// protégeait que l'instant de l'engagement sur SOC bas, pas la phase de
|
||||||
|
// remontée grid_soc_on -> grid_soc_off, ce qui transformait l'hystérésis
|
||||||
|
// 20/30 en hystérésis 20/20,1 et produisait un yoyo EDF de 2 à 4 minutes.
|
||||||
|
// Les scénarios `h-recovery-band-*` ci-dessous sont les garde-fous qui
|
||||||
|
// auraient attrapé cette régression : ils vérifient que la remontée de la
|
||||||
|
// batterie sur réseau n'est JAMAIS interrompue tant que grid_soc_off n'est
|
||||||
|
// pas atteint.
|
||||||
|
|
||||||
function stamp(ms) {
|
function stamp(ms) {
|
||||||
const p = new Intl.DateTimeFormat('fr-FR', {
|
const p = new Intl.DateTimeFormat('fr-FR', {
|
||||||
@@ -23,63 +31,31 @@ function minutesAgo(fixedMs, minutes) {
|
|||||||
return stamp(fixedMs - minutes * 60000);
|
return stamp(fixedMs - minutes * 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const T = '2026-08-22T06:15:00+02:00'; // fenêtre HC (01:24-07:24) fermée mi-session
|
const T = '2026-08-24T06:45:00+02:00'; // heure de l'incident de yoyo du 24/08
|
||||||
const T_MS = new Date(T).getTime();
|
const T_MS = new Date(T).getTime();
|
||||||
|
|
||||||
|
// Base commune : EDF ON sous latch, aucun override EVCS (véhicule débranché),
|
||||||
|
// lecture HA saine. C'est la situation "remontée batterie sur réseau".
|
||||||
|
const recoveryBase = {
|
||||||
|
grid_latch: 'on',
|
||||||
|
grid_state: 'on',
|
||||||
|
night_grid_takeover_needed: 'off',
|
||||||
|
evcs_priority_active: 'off',
|
||||||
|
evcs_source_mode: 'Bloqué',
|
||||||
|
evcs_surplus_state: 'vehicle_not_connected',
|
||||||
|
user_strategy: 'auto_solar_plus',
|
||||||
|
};
|
||||||
|
|
||||||
export const scenarios = [
|
export const scenarios = [
|
||||||
{
|
{
|
||||||
id: 'g-incident-gap-mid-session',
|
id: 'h-engage-low-soc',
|
||||||
label: 'Fenêtre fermée mi-session (stop_source_lost) — min_on NON écoulé',
|
label: 'SOC 18% <= seuil ON 20% : EDF maintenu (engagement batterie basse)',
|
||||||
time: T,
|
time: T,
|
||||||
overrides: {
|
overrides: {
|
||||||
grid_latch: 'on',
|
...recoveryBase,
|
||||||
grid_state: 'on',
|
|
||||||
soc_percent: '30',
|
|
||||||
night_grid_takeover_needed: 'off',
|
|
||||||
evcs_source_mode: 'Bloqué',
|
|
||||||
evcs_surplus_state: 'stop_source_lost',
|
|
||||||
user_strategy: 'auto_solar_plus',
|
|
||||||
last_on_at: minutesAgo(T_MS, 5), // << 25 min de min_on
|
|
||||||
},
|
|
||||||
expect: {
|
|
||||||
state: 'turn_off_evcs_override_released',
|
|
||||||
gridAction: 'turn_off',
|
|
||||||
latchAction: 'turn_off',
|
|
||||||
evcsGridOverride: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'g-incident-gap-idle-wait-source',
|
|
||||||
label: 'Fenêtre fermée mi-session (idle_wait_source) — même bypass générique',
|
|
||||||
time: T,
|
|
||||||
overrides: {
|
|
||||||
grid_latch: 'on',
|
|
||||||
grid_state: 'on',
|
|
||||||
soc_percent: '30',
|
|
||||||
night_grid_takeover_needed: 'off',
|
|
||||||
evcs_source_mode: 'Bloqué',
|
|
||||||
evcs_surplus_state: 'idle_wait_source',
|
|
||||||
user_strategy: 'auto_solar_plus',
|
|
||||||
last_on_at: minutesAgo(T_MS, 5),
|
|
||||||
},
|
|
||||||
expect: {
|
|
||||||
state: 'turn_off_evcs_override_released',
|
|
||||||
gridAction: 'turn_off',
|
|
||||||
latchAction: 'turn_off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'g-baseline-low-soc-no-override',
|
|
||||||
label: 'SOC bas indépendant (18% <= seuil ON 22%) — Fix G ne doit PAS agir',
|
|
||||||
time: T,
|
|
||||||
overrides: {
|
|
||||||
grid_latch: 'on',
|
|
||||||
grid_state: 'on',
|
|
||||||
soc_percent: '18',
|
|
||||||
night_grid_takeover_needed: 'off',
|
|
||||||
evcs_source_mode: '',
|
|
||||||
evcs_surplus_state: '',
|
evcs_surplus_state: '',
|
||||||
user_strategy: 'auto_solar_plus',
|
evcs_source_mode: '',
|
||||||
|
soc_percent: '18',
|
||||||
last_on_at: minutesAgo(T_MS, 5),
|
last_on_at: minutesAgo(T_MS, 5),
|
||||||
},
|
},
|
||||||
expect: {
|
expect: {
|
||||||
@@ -89,17 +65,83 @@ export const scenarios = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'g-regression-fixc-unplugged',
|
id: 'h-recovery-band-no-cut-21',
|
||||||
label: 'Véhicule débranché — Fix C doit toujours gagner (pas absorbé par Fix G)',
|
label: 'ANTI-YOYO : SOC 21% (juste au-dessus du seuil ON), min_on non écoulé — aucune coupure',
|
||||||
time: T,
|
time: T,
|
||||||
overrides: {
|
overrides: {
|
||||||
grid_latch: 'on',
|
...recoveryBase,
|
||||||
grid_state: 'on',
|
// véhicule débranché mais SOC < seuil OFF : Fix C ne doit pas couper,
|
||||||
|
// et rien d'autre ne doit couper non plus. C'est LE point exact où
|
||||||
|
// Fix G coupait et provoquait le yoyo de 2-4 min du 24/08.
|
||||||
|
soc_percent: '21',
|
||||||
|
last_on_at: minutesAgo(T_MS, 5),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
state: 'hold_on_min_runtime',
|
||||||
|
gridAction: 'none',
|
||||||
|
latchAction: 'none',
|
||||||
|
evcsGridOverride: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'h-recovery-band-no-cut-25',
|
||||||
|
label: 'ANTI-YOYO : SOC 25% en pleine bande, min_on écoulé — la remontée doit continuer',
|
||||||
|
time: T,
|
||||||
|
overrides: {
|
||||||
|
...recoveryBase,
|
||||||
|
soc_percent: '25',
|
||||||
|
last_on_at: minutesAgo(T_MS, 120), // min_on largement écoulé
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
state: 'hold_on_until_soc_off',
|
||||||
|
gridAction: 'none',
|
||||||
|
latchAction: 'none',
|
||||||
|
evcsGridOverride: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'h-recovery-complete',
|
||||||
|
label: 'SOC 30% = seuil OFF, min_on écoulé : coupure normale de fin de remontée',
|
||||||
|
time: T,
|
||||||
|
overrides: {
|
||||||
|
...recoveryBase,
|
||||||
|
// véhicule branché mais au repos : on teste la coupure GÉNÉRIQUE de fin
|
||||||
|
// de remontée, pas le raccourci Fix C (débranchement).
|
||||||
|
evcs_surplus_state: 'idle_wait_conditions',
|
||||||
|
soc_percent: '30',
|
||||||
|
last_on_at: minutesAgo(T_MS, 120),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
state: 'turn_off_recovered',
|
||||||
|
gridAction: 'turn_off',
|
||||||
|
latchAction: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'h-override-released-soc-ok',
|
||||||
|
label: 'Override EVCS relâché avec SOC 34% >= seuil OFF : la boucle générique coupe déjà (aucun fix dédié requis)',
|
||||||
|
time: T,
|
||||||
|
overrides: {
|
||||||
|
...recoveryBase,
|
||||||
|
// Cas de l'incident du 21-22/08 tel qu'il aurait dû se présenter :
|
||||||
|
// état transitoire non énuméré, mais SOC au-dessus du seuil OFF.
|
||||||
|
evcs_surplus_state: 'stop_source_lost',
|
||||||
|
soc_percent: '34',
|
||||||
|
last_on_at: minutesAgo(T_MS, 120),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
state: 'turn_off_recovered',
|
||||||
|
gridAction: 'turn_off',
|
||||||
|
latchAction: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'g-regression-fixc-unplugged',
|
||||||
|
label: 'Véhicule débranché + SOC 40% >= seuil OFF : Fix C coupe immédiatement (bypass min_on)',
|
||||||
|
time: T,
|
||||||
|
overrides: {
|
||||||
|
...recoveryBase,
|
||||||
soc_percent: '40',
|
soc_percent: '40',
|
||||||
night_grid_takeover_needed: 'off',
|
|
||||||
evcs_source_mode: 'Bloqué',
|
|
||||||
evcs_surplus_state: 'vehicle_not_connected',
|
|
||||||
user_strategy: 'auto_solar_plus',
|
|
||||||
last_on_at: minutesAgo(T_MS, 5),
|
last_on_at: minutesAgo(T_MS, 5),
|
||||||
},
|
},
|
||||||
expect: {
|
expect: {
|
||||||
@@ -109,16 +151,12 @@ export const scenarios = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'g-regression-fixe2-recovery-done',
|
id: 'g-regression-fixe2-recovery-done',
|
||||||
label: 'Recovery done — Fix E2 doit toujours gagner (pas absorbé par Fix G)',
|
label: 'Recovery done + SOC 40% : Fix E2 coupe immédiatement (bypass min_on)',
|
||||||
time: T,
|
time: T,
|
||||||
overrides: {
|
overrides: {
|
||||||
grid_latch: 'on',
|
...recoveryBase,
|
||||||
grid_state: 'on',
|
|
||||||
soc_percent: '40',
|
|
||||||
night_grid_takeover_needed: 'off',
|
|
||||||
evcs_source_mode: 'Bloqué',
|
|
||||||
evcs_surplus_state: 'night_recovery_done_idle',
|
evcs_surplus_state: 'night_recovery_done_idle',
|
||||||
user_strategy: 'auto_solar_plus',
|
soc_percent: '40',
|
||||||
last_on_at: minutesAgo(T_MS, 5),
|
last_on_at: minutesAgo(T_MS, 5),
|
||||||
},
|
},
|
||||||
expect: {
|
expect: {
|
||||||
@@ -126,43 +164,4 @@ export const scenarios = [
|
|||||||
gridAction: 'turn_off',
|
gridAction: 'turn_off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'g-incident-replay-hc-boundary',
|
|
||||||
label: 'Rejeu incident complet : min_on déjà écoulé aussi — pas seulement un bypass min_on',
|
|
||||||
time: T,
|
|
||||||
overrides: {
|
|
||||||
grid_latch: 'on',
|
|
||||||
grid_state: 'on',
|
|
||||||
soc_percent: '30',
|
|
||||||
night_grid_takeover_needed: 'off',
|
|
||||||
evcs_source_mode: 'Bloqué',
|
|
||||||
evcs_surplus_state: 'stop_source_lost',
|
|
||||||
user_strategy: 'auto_solar_plus',
|
|
||||||
last_on_at: minutesAgo(T_MS, 120), // min_on largement écoulé
|
|
||||||
},
|
|
||||||
expect: {
|
|
||||||
state: 'turn_off_evcs_override_released',
|
|
||||||
gridAction: 'turn_off',
|
|
||||||
latchAction: 'turn_off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'g-glitch-guard-unavailable',
|
|
||||||
label: 'Lecture HA source_mode invalide — garde anti-glitch, Fix G ne doit PAS agir',
|
|
||||||
time: T,
|
|
||||||
overrides: {
|
|
||||||
grid_latch: 'on',
|
|
||||||
grid_state: 'on',
|
|
||||||
soc_percent: '30',
|
|
||||||
night_grid_takeover_needed: 'off',
|
|
||||||
evcs_source_mode: 'unavailable',
|
|
||||||
evcs_surplus_state: 'stop_source_lost',
|
|
||||||
user_strategy: 'auto_solar_plus',
|
|
||||||
last_on_at: minutesAgo(T_MS, 5),
|
|
||||||
},
|
|
||||||
expect: {
|
|
||||||
state: 'hold_on_min_runtime',
|
|
||||||
gridAction: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -777,6 +777,37 @@ export const scenarios = [
|
|||||||
takeoverHelperService: 'turn_off',
|
takeoverHelperService: 'turn_off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'midday-inversion-start-hold',
|
||||||
|
label: 'INVARIANT maintien<=demarrage : midi, SOC 58 dans la bande [40,60], charge 18 A en cours sous abondance → doit CONTINUER (incident 24/08 12:24-12:28)',
|
||||||
|
time: MIDDAY,
|
||||||
|
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 11:30:00', // min_run largement écoulé
|
||||||
|
// Relevé VRM du 24/08 12:25 : PV 10,24 kW, charges 3815 W (dont EVCS 3502 W),
|
||||||
|
// batterie +127 A. SOC 58 % : au-dessus du plancher de démarrage sous
|
||||||
|
// abondance (40 %) mais sous le plancher de maintien de la règle midi
|
||||||
|
// (max(29, 75-15) = 60 %). Avant correctif : arrêt, puis redémarrage au
|
||||||
|
// cycle suivant → cycle limite de 3 min et PV écrêtée.
|
||||||
|
soc_percent: '58', pv_power_w: '10240', pv_power_10m_avg_w: '9800',
|
||||||
|
pv_trend_w: '200', battery_current_a: '127',
|
||||||
|
evcs_current_a: '18', evcs_set_current_a: '18', evcs_command_current_a: '18',
|
||||||
|
evcs_ac_power_w: '3502',
|
||||||
|
house_ac_power_w: '3815', vebus_ac_out_l1_power_w: '3815',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'midday_hc_or_solar',
|
||||||
|
sourceMode: 'solar_only',
|
||||||
|
state: 'charging_step_up_battery',
|
||||||
|
decision: 'step_up',
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 19,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'midday-hc-garde-soc',
|
id: 'midday-hc-garde-soc',
|
||||||
label: 'Midi sans PV, SOC 50 > 35, HC midi autorisé + EDF ON → grid proposé mais gardé (soc>35)',
|
label: 'Midi sans PV, SOC 50 > 35, HC midi autorisé + EDF ON → grid proposé mais gardé (soc>35)',
|
||||||
|
|||||||
67
tools/patch-20260824-revert-g.mjs
Normal file
67
tools/patch-20260824-revert-g.mjs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Patch 24/08/2026 — REVERT de FIX G (22/08/2026).
|
||||||
|
//
|
||||||
|
// Pourquoi : Fix G coupait EDF dès que l'override EVCS disparaissait ET que
|
||||||
|
// SOC > gridSocOn. Le garde `soc > gridSocOn` ne protégeait que l'INSTANT de
|
||||||
|
// l'engagement sur SOC bas (soc <= 20), pas la PHASE DE REMONTÉE 20 -> 30 qui
|
||||||
|
// est 100 % de la durée de vie de ce cas. Résultat en production la nuit du
|
||||||
|
// 23-24/08, véhicule débranché :
|
||||||
|
// SOC 20 -> branche low_soc : EDF ON (bypass cooldown)
|
||||||
|
// SOC 21 -> Fix G : EDF OFF (bypass min_on)
|
||||||
|
// ... cycle de 2 à 4 minutes, MPPT qui flappe à chaque bascule.
|
||||||
|
// Fix G avait de fait remplacé l'hystérésis 20/30 par une hystérésis 20/20,1.
|
||||||
|
//
|
||||||
|
// De plus, sa prémisse était fausse : l'incident du 21-22/08 qui l'avait
|
||||||
|
// motivé n'est pas un problème d'override EVCS mais un problème de bilan
|
||||||
|
// énergétique (couper EDF à 21 % à 06:43 alors que la PV ne couvre la conso
|
||||||
|
// maison qu'à partir de ~09:00 rend le yoyo mécaniquement inévitable, quels
|
||||||
|
// que soient les seuils). Ce sujet est traité séparément.
|
||||||
|
//
|
||||||
|
// Cible : workflows/victron-grid-hysteresis-v1.json (node "Compute Grid Hysteresis").
|
||||||
|
// Rejouable : détecte si déjà retiré.
|
||||||
|
|
||||||
|
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 FILE = path.join(ROOT, 'workflows/victron-grid-hysteresis-v1.json');
|
||||||
|
|
||||||
|
const wf = JSON.parse(fs.readFileSync(FILE, 'utf8'));
|
||||||
|
const node = wf.nodes.find(n => n.name === 'Compute Grid Hysteresis');
|
||||||
|
if (!node) throw new Error('Node "Compute Grid Hysteresis" introuvable');
|
||||||
|
|
||||||
|
let code = node.parameters.jsCode;
|
||||||
|
|
||||||
|
if (!code.includes('turn_off_evcs_override_released')) {
|
||||||
|
console.log('Fix G déjà retiré — rien à faire.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 1. Bloc de décision -------------------------------------------------
|
||||||
|
const decisionStart = code.indexOf('\n// ======================================================\n// FIX G (22/08/2026)');
|
||||||
|
if (decisionStart === -1) throw new Error('En-tête du bloc de décision Fix G introuvable.');
|
||||||
|
|
||||||
|
const decisionEndMarker = " forcedCut = true;\n}\n";
|
||||||
|
const decisionEnd = code.indexOf(decisionEndMarker, decisionStart);
|
||||||
|
if (decisionEnd === -1) throw new Error('Fin du bloc de décision Fix G introuvable.');
|
||||||
|
|
||||||
|
code = code.slice(0, decisionStart) + code.slice(decisionEnd + decisionEndMarker.length);
|
||||||
|
|
||||||
|
// --- 2. Branche notification --------------------------------------------
|
||||||
|
const notifStart = code.indexOf("} else if (state === 'turn_off_evcs_override_released') {");
|
||||||
|
if (notifStart === -1) throw new Error('Branche notification Fix G introuvable.');
|
||||||
|
|
||||||
|
const notifEndMarker = " notifyWanted = true;\n\n";
|
||||||
|
const notifEnd = code.indexOf(notifEndMarker, notifStart);
|
||||||
|
if (notifEnd === -1) throw new Error('Fin de la branche notification Fix G introuvable.');
|
||||||
|
|
||||||
|
code = code.slice(0, notifStart) + code.slice(notifEnd + notifEndMarker.length);
|
||||||
|
|
||||||
|
if (code.includes('turn_off_evcs_override_released')) {
|
||||||
|
throw new Error('Résidus de Fix G après retrait — vérifier manuellement.');
|
||||||
|
}
|
||||||
|
|
||||||
|
node.parameters.jsCode = code;
|
||||||
|
fs.writeFileSync(FILE, JSON.stringify(wf, null, 2) + '\n', 'utf8');
|
||||||
|
console.log('Fix G retiré de', FILE, `(jsCode: ${code.length} caractères)`);
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user