Retrait de Fix G + garde-fous anti-yoyo + diagnostic des deux oscillateurs

Fix G provoquait un yoyo EDF de 2-4 min (coupure a SOC 21 en bypass min_on
contre rallumage a SOC 20 en bypass cooldown) : son garde `soc > gridSocOn`
ne protegeait que l'instant de l'engagement sur SOC bas, pas la phase de
remontee 20 -> 30. jsCode restaure byte-identique a la version pre-Fix-G.

Le fixture de test portait les fallbacks du code (22/35) au lieu des valeurs
de production lues via MCP (20/30), et le scenario de non-regression testait
SOC 18 %, le seul point que Fix G epargnait. Suite reecrite avec les
garde-fous manquants h-recovery-band-no-cut-21/-25. Mutation : Fix G
reapplique -> 4/7 echecs, retire -> 7/7.

Documentation du diagnostic des deux oscillateurs (yoyo EDF et oscillation
de la charge EVCS en plein soleil) dans MANUEL_COMPLET 11.8 et HANDOFF 25.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-08-24 12:20:12 +02:00
parent c9dd79c30f
commit 522f47e821
6 changed files with 321 additions and 124 deletions

View File

@@ -1242,3 +1242,71 @@ 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 ~11:58, PV 7,9 kW).
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. Causes réelles :
1. `battery_discharge_soft_a` = 0 A et `battery_neutral_band_a` = 0 A annulent
la tolérance de −12 A de la bande SOC 40-60 % (`profileBatteryDischargeFloorA
= max(profil, global)`) → bande morte nulle, toute pointe de conso casse
`solarHoldOk` ; avec `min_run_minutes` = 3 → cycle de 3 à 6 min.
2. En `outside_window` le plancher de démarrage est `soc_start_min` = 80 % pour
un SOC de 49 % : seul l'override d'abondance P6 autorise le départ, et il
exige `batteryCurrentA >= +20 A` — que la charge du véhicule fait justement
tomber. **L'action détruit sa propre condition d'autorisation.**
### 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.
---
*Document mis à jour le : 24/08/2026 (~12:20) — Fix G retiré, golden-hysteresis 7/7 (seuils réels 20/30), diagnostic des deux oscillateurs*

View File

@@ -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,71 @@ 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 vers 11:58-12:00 avec PV 7,7-7,9 kW : la borne alterne
`Waiting for start` / 0 A et `Charging` / 7 A. **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), et le plafond calculé
par `WINDOWS_SOURCEV2` ressort à ~20 A. Deux causes réelles :
1. **Bande morte nulle.** `battery_discharge_soft_a` = 0 A et
`battery_neutral_band_a` = 0 A (réglés à 0 le 17/07/2026). Comme
`profileBatteryDischargeFloorA = max(seuil du profil, réglage global)`, la
tolérance de −12 A prévue pour la bande SOC 40-60 % est **annulée** : la
moindre pointe de conso (ballon, lave-vaisselle) fait passer le courant
batterie sous 0 A et casse `solarHoldOk`. Avec `min_run_minutes` = 3, la
période du cycle est de 3 à 6 minutes.
2. **Condition de démarrage auto-destructrice (P6).** En `outside_window`
(11:00-12:24) le plancher de démarrage est `soc_start_min` = 80 % alors que
le SOC est à 49 %. Le seul chemin autorisant le départ est l'override
d'abondance P6, qui exige `pvPowerAvg10m ≥ 4000 W` **et**
`batteryCurrentA ≥ +20 A`. Or brancher la voiture détourne le surplus et
fait tomber le courant batterie sous +20 A : l'action détruit sa propre
condition d'autorisation. Système bistable → oscillateur.
Direction retenue (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

View File

@@ -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",

View File

@@ -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',
},
},
]; ];

View 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