Files
EVCS/tests/golden-acm/scenarios.mjs
Tallagrand Thierry 0ba3cb81ee L Auto Charge Manager a enfin un filet
Troisieme workflow structurant, et le seul sans tests : c est lui qui decide ce
que la batterie a le droit de prendre, donc lui qui plafonnait le parc a 93 %,
et il n etait meme pas suivi dans le depot avant ce matin.

Vingt scenarios sur le patron de la suite hysteresis, avec le vrai code du node
dans une sandbox et un snapshot capture contre Home Assistant plutot
qu invente — la lecon du 24/08, quand un fixture portait les valeurs de repli du
code au lieu des seuils de production et masquait un bug de bande.

Ils suivent l ordre reel des branches et enferment chaque paire dans son
contrefactuel : hiver sombre contre hiver ensoleille, ete secours contre ete
repos, rattrapage de midi contre midi ou le soleil revient, override actif
contre override expire, et le takeover demande alors que le contacteur est
ouvert. Le releve reel du maintien a 2 A de cette nuit y figure, ainsi que le
topoff a 52,4 V pose ce matin.

Cinq mutations, chacune tombant sur un scenario different. Suite cablee au
push, bloquante comme les trois autres.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-09-06 15:31:32 +02:00

207 lines
9.1 KiB
JavaScript

// Scenarios golden pour le node "Compute Charge Targets"
// (workflow "Victron - Auto Charge Manager V1.2", id cTeeHy4ePiVeELLh).
//
// Ce workflow decide ce que la BATTERIE a le droit de prendre : les limites
// DVCC en courant et en tension. Il gouverne donc le haut de la reserve — c'est
// lui qui plafonnait le parc a 93 % de SOC jusqu'au 06/09 — et il n'avait aucun
// test : il n'etait meme pas suivi dans le depot.
//
// Le fixture est un snapshot REEL, capture le 06/09/2026 en rendant le gabarit
// du node "HA Snapshot via Template" contre Home Assistant. Ne pas le remplacer
// par des valeurs inventees : c'est la lecon du 24/08, ou le fixture de
// l'hysteresis portait les valeurs de REPLI du code (22/35) au lieu des seuils
// de production (20/30), ce qui masquait un bug de bande.
//
// ⚠️ ORDRE DES BRANCHES, a garder en tete en lisant les attendus :
// 1. automatisation desactivee
// 2. secours SOC < 20 hors fenetres HC
// 3. takeover EVCS nuit (recover / hold)
// 4. hiver + HC nuit
// 5. ete + HC nuit
// 6. HC midi (rattrapage, seulement si SOC < 85 ET forecast < 8 ET PV < 1500)
// 7. fenetre jour (boost / mid / topoff / hold selon le SOC)
// 8. hors fenetre
// puis l'override se superpose a tout.
const ETE = '2026-07-15';
const HIVER = '2026-01-15';
const h = (jour, hhmm) => `${jour}T${hhmm}:00+02:00`;
const hHiver = (hhmm) => `${HIVER}T${hhmm}:00+01:00`;
// Le parc a quatre modules du 06/09 : le BMS autorise 53,2 V, et nos profils
// restent tous en dessous.
const parcNeuf = { soc_percent: '60', battery_voltage_v: '51.0', battery_current_a: '20' };
export const scenarios = [
// ---------------------------------------------------------------- gardes
{
id: 'acm-automatisation-desactivee',
label: "Toggle d'automatisation OFF : les reglages en place sont conserves tels quels",
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, auto_enabled: 'off',
current_max_charge_current_a: '77', current_max_charge_voltage_v: '50.9' },
expect: { mode: 'automation_disabled', profil: 'manual_keep_current',
courantA: 77, tensionV: 50.9, publieCourant: false, publieTension: false },
},
{
id: 'acm-secours-soc-critique',
label: 'SOC 18 % hors fenetres HC : secours a 60 A / 50,4 V et alerte critique',
time: h(ETE, '10:00'),
overrides: { ...parcNeuf, soc_percent: '18' },
expect: { mode: 'emergency_rescue', profil: 'emergency_rescue',
courantA: 60, tensionV: 50.4, alerte: true },
},
// ------------------------------------------------------- takeover de nuit
{
id: 'acm-takeover-recharge-vers-la-cible',
label: 'Takeover EVCS, EDF ferme, SOC 30 sous la cible 50 : recharge a 30 A',
time: h(ETE, '02:30'),
overrides: { ...parcNeuf, soc_percent: '30', night_takeover: 'on',
edf_switch: 'on', night_target_soc: '50' },
expect: { mode: 'evcs_takeover_recover', profil: 'night_slow',
courantA: 30, tensionV: 50.1 },
},
{
id: 'acm-takeover-maintien-a-la-cible',
label: 'RELEVE 06/09 05:06 : cible atteinte, la batterie est tenue a 2 A pendant que le reseau porte la maison',
time: h(ETE, '05:06'),
overrides: { ...parcNeuf, soc_percent: '55', night_takeover: 'on',
edf_switch: 'on', night_target_soc: '49.4' },
expect: { mode: 'evcs_takeover_hold', profil: 'takeover_hold',
courantA: 2, tensionV: 50.1 },
},
{
id: 'acm-takeover-cible-plafonnee-a-95',
label: 'GARDE : une cible aberrante de 120 % est ramenee a 95, donc SOC 96 tient deja',
time: h(ETE, '03:00'),
overrides: { ...parcNeuf, soc_percent: '96', night_takeover: 'on',
edf_switch: 'on', night_target_soc: '120' },
expect: { mode: 'evcs_takeover_hold', courantA: 2 },
},
{
id: 'acm-takeover-sans-edf-ne-mord-pas',
label: "CONTREFACTUEL : takeover demande mais contacteur OUVERT — on ne bride pas la batterie sur du reseau absent",
time: h(ETE, '03:00'),
overrides: { ...parcNeuf, soc_percent: '30', night_takeover: 'on',
edf_switch: 'off', night_target_soc: '50' },
expect: { mode: 'summer_night_idle' },
},
// ------------------------------------------------------------ nuit HC
{
id: 'acm-hiver-nuit-forecast-sombre',
label: 'Hiver, HC nuit, demain sombre (5 kWh) : on remonte vers 80 %',
time: hHiver('03:00'),
overrides: { ...parcNeuf, soc_percent: '50',
today_remaining_kwh: '0', tomorrow_kwh: '5' },
expect: { mode: 'winter_night_charge_window', profil: 'night_slow', courantA: 30 },
},
{
id: 'acm-hiver-nuit-forecast-ensoleille',
label: 'PAIRE : meme nuit d hiver mais demain ensoleille (30 kWh), cible 40 % deja depassee',
time: hHiver('03:00'),
overrides: { ...parcNeuf, soc_percent: '50',
today_remaining_kwh: '0', tomorrow_kwh: '30' },
expect: { mode: 'winter_night_idle' },
},
{
id: 'acm-ete-nuit-secours',
label: 'Ete, HC nuit, SOC 20 sous le seuil de 22 : secours',
time: h(ETE, '03:00'),
overrides: { ...parcNeuf, soc_percent: '20' },
expect: { mode: 'summer_night_rescue_window', profil: 'night_slow' },
},
{
id: 'acm-ete-nuit-repos',
label: 'PAIRE : meme nuit d ete a SOC 50, aucun besoin reseau',
time: h(ETE, '03:00'),
overrides: { ...parcNeuf, soc_percent: '50' },
expect: { mode: 'summer_night_idle' },
},
// ------------------------------------------------------------ midi HC
{
id: 'acm-midi-rattrapage',
label: 'HC midi, SOC 60, journee sombre (5 kWh) et PV a 800 W : rattrapage reseau a 30 A',
time: h(ETE, '13:00'),
overrides: { ...parcNeuf, soc_percent: '60', today_remaining_kwh: '5', pv_power_w: '800' },
expect: { mode: 'midday_catchup_window', profil: 'midday_grid_catchup',
courantA: 30, tensionV: 50.8 },
},
{
id: 'acm-midi-pas-de-rattrapage-si-le-soleil-revient',
label: 'CONTREFACTUEL : meme midi mais PV a 2000 W — le rattrapage ne mord plus, on repasse en profil jour',
time: h(ETE, '13:00'),
overrides: { ...parcNeuf, soc_percent: '60', today_remaining_kwh: '5', pv_power_w: '2000' },
expect: { mode: 'day_pv_boost', profil: 'pv_boost' },
},
// ------------------------------------------------------------ jour
{
id: 'acm-jour-boost',
label: 'Jour, SOC 60 sous 80 : capture PV maximale, 130 A a 51,5 V',
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, soc_percent: '60', pv_power_w: '4000' },
expect: { mode: 'day_pv_boost', profil: 'pv_boost', courantA: 130, tensionV: 51.5 },
},
{
id: 'acm-jour-mid',
label: 'Jour, SOC 85 entre 80 et 90 : 110 A a 51,2 V',
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, soc_percent: '85', pv_power_w: '4000', battery_voltage_v: '50.5' },
expect: { mode: 'day_pv_mid', profil: 'pv_mid', courantA: 110, tensionV: 51.2 },
},
{
id: 'acm-jour-topoff-52v4',
label: "LE CORRECTIF DU 06/09 : SOC 93 dans la tranche 90-95, la tension monte a 52,4 V et le parc reprend de la charge",
time: h(ETE, '15:00'),
overrides: { ...parcNeuf, soc_percent: '93', pv_power_w: '4000', battery_voltage_v: '51.1' },
expect: { mode: 'day_pv_topoff', profil: 'pv_topoff', courantA: 80, tensionV: 52.4 },
},
{
id: 'acm-jour-hold-au-dessus-de-95',
label: "GARDE : au-dessus de 95 % on redescend a 50,6 V — on atteint le haut, on n'y sejourne pas",
time: h(ETE, '15:00'),
overrides: { ...parcNeuf, soc_percent: '96', pv_power_w: '4000' },
expect: { mode: 'day_pv_hold', profil: 'pv_hold', courantA: 30, tensionV: 50.6 },
},
{
id: 'acm-hors-fenetre',
label: 'Hors fenetre utile (21 h en ete) : profil de maintien',
time: h(ETE, '21:00'),
overrides: { ...parcNeuf, soc_percent: '70' },
expect: { mode: 'off_window_idle', profil: 'pv_hold', courantA: 30, tensionV: 50.6 },
},
// ------------------------------------------------------------ override
{
id: 'acm-override-manuel-actif',
label: 'Override manuel valide : les valeurs saisies prennent la main sur le profil calcule',
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, soc_percent: '60', override_enabled: 'on',
override_profile: 'manual', override_current_a: '12', override_voltage_v: '50.0',
override_until: '2026-07-15 23:00:00' },
expect: { overrideActif: true, profil: 'override_manual', courantA: 12, tensionV: 50 },
},
{
id: 'acm-override-expire-sans-effet',
label: "CONTREFACTUEL : meme override mais l'echeance est passee — le profil calcule reprend",
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, soc_percent: '60', override_enabled: 'on',
override_profile: 'manual', override_current_a: '12', override_voltage_v: '50.0',
override_until: '2026-07-15 09:00:00' },
expect: { overrideActif: false, mode: 'day_pv_boost', courantA: 130 },
},
// ------------------------------------------------------------ publication
{
id: 'acm-pas-de-publication-si-identique',
label: "GARDE : cible egale a ce qui est deja regle — on n'ecrit ni le courant ni la tension",
time: h(ETE, '21:00'),
overrides: { ...parcNeuf, soc_percent: '70',
current_max_charge_current_a: '30', current_max_charge_voltage_v: '50.6' },
expect: { publieCourant: false, publieTension: false },
},
];