Files
EVCS/tests/golden-hysteresis/scenarios.mjs
Tallagrand Thierry 84b9cfef8b Le reseau vient quand la maison demande plus que la batterie ne sait donner
Quatre modules : le BMS annonce 100 A de decharge sous 51 V, soit
5,1 kW, quand la maison a deja pointe a 8 kW en cuisinant. Contacteur
ouvert, la batterie est seule et se fait tirer au-dela de sa limite.

Le declenchement porte sur la CHARGE MAISON, jamais sur le courant
batterie : fermer le contacteur fait chuter ce courant, donc s'en
servir comme critere de relachement ferait que la decision detruirait
sa propre cause. Fermeture a 80 % du plafond, relachement sous 50 %,
les deux seuils derives du plafond annonce par le BMS - ils suivront
donc le parc sans qu'on y touche.

Les tests ont revele un vrai defaut au passage : les trois coupures
immediates s'executent avant la cascade et auraient ouvert le
contacteur que la regle venait de fermer. Elles cedent desormais
devant la charge maison. Preuve par mutation : sans cette garde, un
scenario tombe.

Plafond illisible => regle inerte, comportement historique intact.
11/11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-09-05 19:50:23 +02:00

242 lines
7.6 KiB
JavaScript

// 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',
};
// Plafond de decharge REEL depuis le retrait du pack ancien (05/09/2026) :
// quatre modules, 100 A annonces par le BMS sous 51 V, soit 5100 W. Les seuils
// en decoulent — 4080 W pour fermer, 2550 W pour relacher — et se deplaceront
// tout seuls le jour ou le parc changera.
const parcQuatreModules = {
battery_max_discharge_a: '100',
battery_voltage_v: '51',
};
export const scenarios = [
{
id: 'h-charge-au-dessus-du-parc-ferme',
label: 'Cuisson a 4500 W contre 5100 W de plafond BMS : le reseau vient prendre la difference',
time: T,
overrides: {
...recoveryBase,
...parcQuatreModules,
grid_latch: 'off',
grid_state: 'off',
soc_percent: '91',
house_ac_power_w: '4500',
},
expect: {
state: 'turn_on_charge_au_dessus_du_parc',
gridAction: 'turn_on',
latchAction: 'turn_on',
},
},
{
id: 'h-charge-encore-elevee-maintient',
label: 'Charge retombee a 3000 W, entre les deux seuils : on garde le reseau au lieu de le lacher',
time: T,
overrides: {
...recoveryBase,
...parcQuatreModules,
soc_percent: '91',
house_ac_power_w: '3000',
last_on_at: minutesAgo(T_MS, 40),
},
expect: {
state: 'hold_on_charge_encore_elevee',
gridAction: 'none',
},
},
{
id: 'h-charge-retombee-relache',
label: 'CONTREFACTUEL : meme situation a 2000 W, sous le seuil de relachement — le reseau est rendu (ici par Fix C, le vehicule etant debranche)',
time: T,
overrides: {
...recoveryBase,
...parcQuatreModules,
soc_percent: '91',
house_ac_power_w: '2000',
last_on_at: minutesAgo(T_MS, 40),
},
expect: {
state: 'turn_off_vehicle_unplugged',
gridAction: 'turn_off',
},
},
{
id: 'h-plafond-inconnu-aucune-action',
label: 'Plafond BMS illisible : la regle est inerte, le comportement historique tient',
time: T,
overrides: {
...recoveryBase,
soc_percent: '91',
house_ac_power_w: '4500',
last_on_at: minutesAgo(T_MS, 40),
},
expect: {
state: 'turn_off_vehicle_unplugged',
gridAction: 'turn_off',
},
},
{
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',
},
},
];