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:
@@ -1,14 +1,22 @@
|
||||
// Scénarios golden pour le node "Compute Grid Hysteresis"
|
||||
// (workflow "Victron - Battery / Grid Hysteresis Fix V1").
|
||||
//
|
||||
// Contexte (Fix G, 22/08/2026) : incident nuit 21-22/08 — la fenêtre HC nuit
|
||||
// se ferme pendant que le véhicule est encore en session active (pas encore
|
||||
// flag "charge terminée"). Le contrôleur EVCS relâche correctement son
|
||||
// override (helper night_grid_takeover_needed -> off, source_mode -> Bloqué)
|
||||
// mais émet un état transitoire générique (stop_source_lost / idle_wait_source)
|
||||
// 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
|
||||
// grid_soc_off (35%), potentiellement des heures.
|
||||
// Seuils du fixture = valeurs RÉELLES de production (vérifiées via MCP
|
||||
// ha-get-state le 24/08/2026) : grid_soc_on = 20, grid_soc_off = 30,
|
||||
// min_on = 25 min, cooldown = 15 min. Ne PAS utiliser les valeurs de repli
|
||||
// codées dans le node (22/35) : elles ne correspondent à rien en production
|
||||
// et masquent les bugs de bande.
|
||||
//
|
||||
// Historique : cette suite a été créée le 22/08/2026 pour valider "Fix G"
|
||||
// (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) {
|
||||
const p = new Intl.DateTimeFormat('fr-FR', {
|
||||
@@ -23,63 +31,31 @@ function minutesAgo(fixedMs, minutes) {
|
||||
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();
|
||||
|
||||
// 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 = [
|
||||
{
|
||||
id: 'g-incident-gap-mid-session',
|
||||
label: 'Fenêtre fermée mi-session (stop_source_lost) — min_on NON écoulé',
|
||||
id: 'h-engage-low-soc',
|
||||
label: 'SOC 18% <= seuil ON 20% : EDF maintenu (engagement batterie basse)',
|
||||
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, 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: '',
|
||||
...recoveryBase,
|
||||
evcs_surplus_state: '',
|
||||
user_strategy: 'auto_solar_plus',
|
||||
evcs_source_mode: '',
|
||||
soc_percent: '18',
|
||||
last_on_at: minutesAgo(T_MS, 5),
|
||||
},
|
||||
expect: {
|
||||
@@ -89,17 +65,83 @@ export const scenarios = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'g-regression-fixc-unplugged',
|
||||
label: 'Véhicule débranché — Fix C doit toujours gagner (pas absorbé par Fix G)',
|
||||
id: 'h-recovery-band-no-cut-21',
|
||||
label: 'ANTI-YOYO : SOC 21% (juste au-dessus du seuil ON), min_on non écoulé — aucune coupure',
|
||||
time: T,
|
||||
overrides: {
|
||||
grid_latch: 'on',
|
||||
grid_state: 'on',
|
||||
...recoveryBase,
|
||||
// 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',
|
||||
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),
|
||||
},
|
||||
expect: {
|
||||
@@ -109,16 +151,12 @@ export const scenarios = [
|
||||
},
|
||||
{
|
||||
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,
|
||||
overrides: {
|
||||
grid_latch: 'on',
|
||||
grid_state: 'on',
|
||||
soc_percent: '40',
|
||||
night_grid_takeover_needed: 'off',
|
||||
evcs_source_mode: 'Bloqué',
|
||||
...recoveryBase,
|
||||
evcs_surplus_state: 'night_recovery_done_idle',
|
||||
user_strategy: 'auto_solar_plus',
|
||||
soc_percent: '40',
|
||||
last_on_at: minutesAgo(T_MS, 5),
|
||||
},
|
||||
expect: {
|
||||
@@ -126,43 +164,4 @@ export const scenarios = [
|
||||
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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user