La regle du 05/09 ferme quand la maison depasse 80 % du plafond de decharge du parc — elle a mordu au bon instant hier soir, a -97,4 A pour 100 A autorises. Mais elle relachait sur un seul echantillon sous 50 %, et la soiree offrait quatorze occasions de le faire. Le troisieme pic est tombe deux minutes avant l'ouverture reelle : decale de cinq minutes, elle refermait, et chaque aller-retour du contacteur coute 50 a 150 Wh. Les accalmies mesurees sont franchement bimodales — treize de 5 min au plus pendant la cuisson, une de 201 min apres. Le relachement se mesure donc en duree, et la duree vient du bilan energetique : 20 min tant que la batterie est condamnee a toucher son plancher avant les heures creuses (l'energie reseau du soir est due de toute facon, la tenir ne change que son heure), 6 min des qu'elle passerait la nuit seule (chaque kWh du soir deplacerait alors un kWh d'heures creuses vers des heures pleines). Premiere lecture du bilan par le workflow d'hysteresis. Le dwell se compte sur la maison, jamais sur le contacteur : l'horodatage est ecrit qu'il soit ouvert ou ferme, sans quoi la decision detruirait sa propre cause. Et parce que la tension batterie s'est tue 27 minutes hier soir en pleine cuisson, le plafond retombe sur la tension nominale du parc, et a defaut l'horodatage — un fait persiste — tient la protection jusqu'au bout du dwell. Golden-hysteresis 11 -> 20. Six mutations, chacune tombant sur un jeu different. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
463 lines
15 KiB
JavaScript
463 lines
15 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);
|
|
}
|
|
|
|
// Le bilan energetique publie son horodatage au format "DD/MM HH:mm".
|
|
function bilanRunAgo(fixedMs, minutes) {
|
|
const p = new Intl.DateTimeFormat('fr-FR', {
|
|
timeZone: 'Europe/Paris', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', hour12: false,
|
|
}).formatToParts(new Date(fixedMs - minutes * 60000));
|
|
const g = t => p.find(x => x.type === t).value;
|
|
return `${g('day')}/${g('month')} ${g('hour')}:${g('minute')}`;
|
|
}
|
|
|
|
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',
|
|
},
|
|
},
|
|
// =====================================================
|
|
// RELACHEMENT SUR UNE DUREE (06/09/2026)
|
|
// =====================================================
|
|
// Mesure de la soiree du 05/09, seuil de relachement 2550 W, pas d'une
|
|
// minute : pendant la cuisson (20:33 -> 21:37) treize accalmies, la plus
|
|
// longue de CINQ minutes ; apres la cuisson (21:40 -> 01:00) une seule
|
|
// accalmie de 201 minutes. La distribution est bimodale, le fosse est net,
|
|
// et le relachement instantane tombait treize fois dedans.
|
|
//
|
|
// Le releve exact de cette soiree : SOC 65 %, bilan exigeant 92 % pour
|
|
// atteindre le croisement PV — la reserve est deja condamnee, donc le
|
|
// reseau du soir est du de toute facon et le long dwell s'applique.
|
|
{
|
|
id: 'h-dwell-accalmie-de-cuisson-ne-relache-pas',
|
|
label: "LE CAS REEL : accalmie de 5 min au milieu du service (la plus longue mesuree) — le reseau n'est PAS rendu",
|
|
time: T,
|
|
overrides: {
|
|
...recoveryBase,
|
|
...parcQuatreModules,
|
|
soc_percent: '65',
|
|
house_ac_power_w: '2000',
|
|
last_on_at: minutesAgo(T_MS, 40),
|
|
charge_high_last_at: minutesAgo(T_MS, 5),
|
|
balance_required_soc: '92',
|
|
balance_last_run: bilanRunAgo(T_MS, 3),
|
|
},
|
|
expect: {
|
|
state: 'hold_on_charge_encore_elevee',
|
|
gridAction: 'none',
|
|
dwellMinutes: 20,
|
|
dwellEcoule: false,
|
|
reserveDejaCondamnee: true,
|
|
setChargeHighLastAt: false,
|
|
},
|
|
},
|
|
{
|
|
id: 'h-dwell-ecoule-relache',
|
|
label: 'CONTREFACTUEL : meme soiree, mais la maison est calme depuis 25 min — le reseau est rendu',
|
|
time: T,
|
|
overrides: {
|
|
...recoveryBase,
|
|
...parcQuatreModules,
|
|
soc_percent: '65',
|
|
house_ac_power_w: '2000',
|
|
last_on_at: minutesAgo(T_MS, 40),
|
|
charge_high_last_at: minutesAgo(T_MS, 25),
|
|
balance_required_soc: '92',
|
|
balance_last_run: bilanRunAgo(T_MS, 3),
|
|
},
|
|
expect: {
|
|
state: 'turn_off_vehicle_unplugged',
|
|
gridAction: 'turn_off',
|
|
dwellMinutes: 20,
|
|
dwellEcoule: true,
|
|
reserveDejaCondamnee: true,
|
|
},
|
|
},
|
|
|
|
// Les trois scenarios suivants sont a 8 MINUTES de calme, entre le dwell
|
|
// court (6) et le long (20) : seul l'etat du bilan les separe.
|
|
{
|
|
id: 'h-dwell-long-quand-la-reserve-est-condamnee',
|
|
label: 'Bilan frais, SOC 65 % sous les 92 % requis : dwell long, on garde le reseau a 8 min',
|
|
time: T,
|
|
overrides: {
|
|
...recoveryBase,
|
|
...parcQuatreModules,
|
|
soc_percent: '65',
|
|
house_ac_power_w: '2000',
|
|
last_on_at: minutesAgo(T_MS, 40),
|
|
charge_high_last_at: minutesAgo(T_MS, 8),
|
|
balance_required_soc: '92',
|
|
balance_last_run: bilanRunAgo(T_MS, 3),
|
|
},
|
|
expect: {
|
|
state: 'hold_on_charge_encore_elevee',
|
|
gridAction: 'none',
|
|
dwellMinutes: 20,
|
|
reserveDejaCondamnee: true,
|
|
},
|
|
},
|
|
{
|
|
id: 'h-dwell-court-quand-la-reserve-tient',
|
|
label: 'PAIRE : bilan frais mais SOC 80 % au-dessus des 40 % requis — chaque kWh du soir couterait, dwell court, on rend a 8 min',
|
|
time: T,
|
|
overrides: {
|
|
...recoveryBase,
|
|
...parcQuatreModules,
|
|
soc_percent: '80',
|
|
house_ac_power_w: '2000',
|
|
last_on_at: minutesAgo(T_MS, 40),
|
|
charge_high_last_at: minutesAgo(T_MS, 8),
|
|
balance_required_soc: '40',
|
|
balance_last_run: bilanRunAgo(T_MS, 3),
|
|
},
|
|
expect: {
|
|
state: 'turn_off_vehicle_unplugged',
|
|
gridAction: 'turn_off',
|
|
dwellMinutes: 6,
|
|
reserveDejaCondamnee: false,
|
|
},
|
|
},
|
|
{
|
|
id: 'h-dwell-bilan-perime-duree-courte',
|
|
label: 'GARDE DE FRAICHEUR : memes chiffres que le dwell long, mais bilan vieux de 2 h — on retombe sur la duree courte',
|
|
time: T,
|
|
overrides: {
|
|
...recoveryBase,
|
|
...parcQuatreModules,
|
|
soc_percent: '65',
|
|
house_ac_power_w: '2000',
|
|
last_on_at: minutesAgo(T_MS, 40),
|
|
charge_high_last_at: minutesAgo(T_MS, 8),
|
|
balance_required_soc: '92',
|
|
balance_last_run: bilanRunAgo(T_MS, 120),
|
|
},
|
|
expect: {
|
|
state: 'turn_off_vehicle_unplugged',
|
|
gridAction: 'turn_off',
|
|
dwellMinutes: 6,
|
|
reserveDejaCondamnee: false,
|
|
},
|
|
},
|
|
{
|
|
id: 'h-dwell-horodatage-ecrit-quand-la-charge-est-haute',
|
|
label: "L'horodatage suit la MAISON : charge haute contacteur OUVERT — il est ecrit quand meme",
|
|
time: T,
|
|
overrides: {
|
|
...recoveryBase,
|
|
...parcQuatreModules,
|
|
grid_latch: 'off',
|
|
grid_state: 'off',
|
|
soc_percent: '65',
|
|
house_ac_power_w: '3000',
|
|
charge_high_last_at: minutesAgo(T_MS, 30),
|
|
},
|
|
expect: {
|
|
setChargeHighLastAt: true,
|
|
},
|
|
},
|
|
// =====================================================
|
|
// CAPTEURS MUETS (releve du 05/09/2026)
|
|
// =====================================================
|
|
// De 21:22:21 a 21:49:35, `sensor.victron_battery_current` et
|
|
// `sensor.victron_battery_voltage` n'ont RIEN publie — 27 minutes — pendant
|
|
// que `sensor.victron_consumption_power_l1` continuait normalement. Un
|
|
// courant fige a -73,2 A pendant que la maison oscille entre 1,4 et 3,4 kW
|
|
// est physiquement impossible : la valeur etait perimee. Sans repli, la
|
|
// protection s'evanouissait pile a l'heure du diner.
|
|
{
|
|
id: 'h-capteur-tension-muette-la-regle-tient',
|
|
label: 'Tension du parc muette, plafond en amperes lisible : nominal 51 V, la regle continue',
|
|
time: T,
|
|
overrides: {
|
|
...recoveryBase,
|
|
battery_max_discharge_a: '100',
|
|
battery_voltage_v: 'unavailable',
|
|
soc_percent: '65',
|
|
house_ac_power_w: '2000',
|
|
last_on_at: minutesAgo(T_MS, 40),
|
|
charge_high_last_at: minutesAgo(T_MS, 5),
|
|
balance_required_soc: '92',
|
|
balance_last_run: bilanRunAgo(T_MS, 3),
|
|
},
|
|
expect: {
|
|
state: 'hold_on_charge_encore_elevee',
|
|
gridAction: 'none',
|
|
plafondDechargeW: 5100,
|
|
},
|
|
},
|
|
{
|
|
id: 'h-capteur-tout-muet-le-dwell-tient',
|
|
label: 'Plafond ET tension muets en pleine protection : l horodatage persiste, on tient jusqu au bout du dwell',
|
|
time: T,
|
|
overrides: {
|
|
...recoveryBase,
|
|
battery_max_discharge_a: 'unavailable',
|
|
battery_voltage_v: 'unavailable',
|
|
soc_percent: '65',
|
|
house_ac_power_w: '2000',
|
|
last_on_at: minutesAgo(T_MS, 40),
|
|
charge_high_last_at: minutesAgo(T_MS, 5),
|
|
balance_required_soc: '92',
|
|
balance_last_run: bilanRunAgo(T_MS, 3),
|
|
},
|
|
expect: {
|
|
state: 'hold_on_charge_encore_elevee',
|
|
gridAction: 'none',
|
|
plafondDechargeW: 0,
|
|
},
|
|
},
|
|
{
|
|
id: 'h-capteur-tout-muet-memoire-expiree',
|
|
label: 'CONTREFACTUEL : memes capteurs muets mais derniere charge haute il y a 30 min — la memoire a expire, le reseau est rendu',
|
|
time: T,
|
|
overrides: {
|
|
...recoveryBase,
|
|
battery_max_discharge_a: 'unavailable',
|
|
battery_voltage_v: 'unavailable',
|
|
soc_percent: '65',
|
|
house_ac_power_w: '2000',
|
|
last_on_at: minutesAgo(T_MS, 40),
|
|
charge_high_last_at: minutesAgo(T_MS, 30),
|
|
balance_required_soc: '92',
|
|
balance_last_run: bilanRunAgo(T_MS, 3),
|
|
},
|
|
expect: {
|
|
state: 'turn_off_vehicle_unplugged',
|
|
gridAction: 'turn_off',
|
|
},
|
|
},
|
|
];
|