// Scénarios golden pour le node "Compute Grid Hysteresis" // (workflow "Victron - Battery / Grid Hysteresis Fix V1"). // // 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', { timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }).formatToParts(new Date(ms)); const g = t => p.find(x => x.type === t).value; return `${g('year')}-${g('month')}-${g('day')} ${g('hour')}:${g('minute')}:${g('second')}`; } function minutesAgo(fixedMs, minutes) { return stamp(fixedMs - minutes * 60000); } 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: 'h-engage-low-soc', label: 'SOC 18% <= seuil ON 20% : EDF maintenu (engagement batterie basse)', time: T, overrides: { ...recoveryBase, evcs_surplus_state: '', evcs_source_mode: '', soc_percent: '18', last_on_at: minutesAgo(T_MS, 5), }, expect: { state: 'hold_on_low_soc', gridAction: 'none', latchAction: 'none', }, }, { 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: { ...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', last_on_at: minutesAgo(T_MS, 5), }, expect: { state: 'turn_off_vehicle_unplugged', gridAction: 'turn_off', }, }, { id: 'g-regression-fixe2-recovery-done', label: 'Recovery done + SOC 40% : Fix E2 coupe immédiatement (bypass min_on)', time: T, overrides: { ...recoveryBase, evcs_surplus_state: 'night_recovery_done_idle', soc_percent: '40', last_on_at: minutesAgo(T_MS, 5), }, expect: { state: 'turn_off_recovery_done', gridAction: 'turn_off', }, }, ];