// 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. 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-22T06:15:00+02:00'; // fenêtre HC (01:24-07:24) fermée mi-session const T_MS = new Date(T).getTime(); export const scenarios = [ { id: 'g-incident-gap-mid-session', label: 'Fenêtre fermée mi-session (stop_source_lost) — min_on NON écoulé', 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: '', evcs_surplus_state: '', user_strategy: 'auto_solar_plus', last_on_at: minutesAgo(T_MS, 5), }, expect: { state: 'hold_on_low_soc', gridAction: 'none', latchAction: 'none', }, }, { id: 'g-regression-fixc-unplugged', label: 'Véhicule débranché — Fix C doit toujours gagner (pas absorbé par Fix G)', time: T, overrides: { grid_latch: 'on', grid_state: 'on', 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: { state: 'turn_off_vehicle_unplugged', gridAction: 'turn_off', }, }, { id: 'g-regression-fixe2-recovery-done', label: 'Recovery done — Fix E2 doit toujours gagner (pas absorbé par Fix G)', time: T, overrides: { grid_latch: 'on', grid_state: 'on', soc_percent: '40', night_grid_takeover_needed: 'off', evcs_source_mode: 'Bloqué', evcs_surplus_state: 'night_recovery_done_idle', user_strategy: 'auto_solar_plus', last_on_at: minutesAgo(T_MS, 5), }, expect: { state: 'turn_off_recovery_done', 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', }, }, ];