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:
68
HANDOFF.md
68
HANDOFF.md
@@ -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*
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
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
Reference in New Issue
Block a user