Files
EVCS/tests/energy-balance/scenarios-balance.mjs
Tallagrand Thierry be1b12969e Vehicule branche a midi : JEEP, VW et moto ZERO (calendrier, capacite et puissance par vehicule)
ZERO ajoutee au selecteur avec son heure de depart (P9). Le bilan lit la
capacite et la puissance de charge mesurees du vehicule selectionne ; 0 =
inconnue (regle muette / pas de borne). Bilan 64/64, golden 123/123.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uJ47NxgfpGnuuMr3f2n7Z
2026-09-28 14:05:04 +02:00

516 lines
22 KiB
JavaScript

// Scénarios du nœud BALANCE.
// Le scénario central est un REJEU de la matinée du 24/08/2026 : la production
// horaire est celle réellement mesurée ce jour-là (heures 06-09 h non
// censurées, donc fiables), le SOC est celui relevé à 06:43, et la capacité
// est celle du parc réel (7 modules actifs, ~15 kWh).
// C'est la situation qui a déclenché tout le chantier : le seuil fixe de 30 %
// et le yoyo EDF de 2 à 4 minutes.
const P50 = [0.70,0.70,0.70,0.70,0.60,0.70,0.70,0.80,0.90,1.40,1.40,2.00,
2.40,1.90,2.10,1.90,1.80,2.00,1.50,1.70,1.60,1.30,1.30,1.10];
const P80 = [1.00,0.70,0.70,0.70,0.70,0.80,1.30,1.80,1.70,2.00,2.50,3.10,
3.50,2.60,2.50,2.30,2.20,2.40,2.30,1.90,1.90,1.60,1.40,1.20];
// Production horaire réellement mesurée le 24/08/2026.
const PROD_24_08 = {
6: 0.03, 7: 0.65, 8: 2.70, 9: 5.62, 10: 7.17, 11: 9.33,
12: 4.37, 13: 2.10, 14: 1.20, 15: 0.90, 16: 0.70, 17: 0.50,
18: 0.30, 19: 0.10, 20: 0.00, 21: 0.00, 22: 0.00, 23: 0.00,
};
function horizonFrom(startIso, hours, production) {
const h0 = Math.floor(Date.parse(startIso) / 3600000) * 3600000;
return Array.from({ length: hours }, (_, i) => {
const ms = h0 + i * 3600000;
// parseInt et non Number : en fr-FR le format renvoie « 06 h ».
const hour = Number.parseInt(new Intl.DateTimeFormat('fr-FR', {
timeZone: 'Europe/Paris', hour: '2-digit', hour12: false,
}).format(new Date(ms)), 10);
return {
startMs: ms,
hour,
expectedKwh: production[hour] ?? 0,
shapeQuality: hour >= 12 && hour <= 19 ? 'minoree' : 'fiable',
};
});
}
// ======== TENIR JUSQU'A LA PROCHAINE FENETRE HC (28/09/2026) ========
// Donnees REELLES de la nuit du 26-27/09 : profils de conso publies ce jour-la,
// prevision horaire Forecast.Solar dont disposait le systeme a 01:15 (25,1 kWh
// pour le 27), parc de 13 modules. La variante « sombre » ramene la journee a
// 18 kWh, ce qu'annoncait VRM pour le 27 (22 kWh reels).
const P50_2709 = [1.10,0.80,0.70,0.60,0.70,0.70,0.70,0.70,0.80,0.90,1.50,1.70,1.40,1.80,1.50,1.20,1.10,0.90,0.90,1.10,1.70,1.50,1.40,1.20];
const P80_2709 = [1.20,1.10,0.90,0.80,0.80,0.80,1.00,0.80,0.90,1.30,2.10,2.70,2.80,2.70,2.30,2.00,1.90,1.50,1.50,1.50,2.10,2.00,2.00,1.60];
const FS_2709 = [0,0,0,0,0,0,0,0,0.24,1.06,1.87,2.51,2.97,3.26,3.28,3.07,2.67,2.11,1.38,0.61,0.06,0,0,0];
const FS_2809 = [0,0,0,0,0,0,0,0,0.23,1.03,1.85,2.49,2.97,3.26,3.29,3.07,2.65,2.08,1.35,0.60,0.06,0,0,0];
function horizon2709(startIso, hours, facteur27 = 1) {
const h0 = Math.floor(Date.parse(startIso) / 3600000) * 3600000;
return Array.from({ length: hours }, (_, i) => {
const ms = h0 + i * 3600000;
const hour = Number.parseInt(new Intl.DateTimeFormat('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', hour12: false }).format(new Date(ms)), 10);
const le27 = new Date(ms).toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' }) === '2026-09-27';
return { startMs: ms, hour, expectedKwh: le27 ? FS_2709[hour] * facteur27 : FS_2809[hour], shapeQuality: 'fiable' };
});
}
function snap2709(nowIso, soc, fenetre, facteur27 = 1) {
const s = { nowMs: Date.parse(nowIso), consumption_p50: P50_2709, consumption_p80: P80_2709,
horizon: horizon2709(nowIso, 40, facteur27), soc_percent: soc, usable_capacity_kwh: 23,
soc_floor_percent: 22, modules_en_service: 13 };
if (fenetre) Object.assign(s, { hc_window_end_ms: Date.parse(fenetre[0]),
next_hc_start_ms: Date.parse(fenetre[1]), next_night_hc_start_ms: Date.parse(fenetre[2]) });
return s;
}
const NUIT_2709 = ['2026-09-27T07:24:00+02:00', '2026-09-27T12:24:00+02:00', '2026-09-28T01:24:00+02:00'];
const MIDI_2709 = ['2026-09-27T14:24:00+02:00', '2026-09-28T01:24:00+02:00', '2026-09-28T01:24:00+02:00'];
const SOMBRE = 18 / 25.1;
// JEEP branchee a midi, depart 15:30 (28/09) : niveau saisi, capacite mesuree
// 8,7 kWh, rien de delivre depuis la saisie sauf mention.
function jeep(snapshot, niveau, extra = {}) {
return Object.assign(snapshot, { vehicle_connected: true,
vehicle_departure_ms: Date.parse('2026-09-27T15:30:00+02:00'), vehicle_level_pct: niveau,
vehicle_level_valid: true, vehicle_capacity_kwh: 8.7, vehicle_charger_kw: 3.7,
vehicle_delivered_since_level_kwh: 0 }, extra);
}
export const balanceScenarios = [
{
// Le parc a change deux fois en une semaine et rechangera cet hiver : le
// nombre de modules se lit dans un helper au lieu d'etre code. Ce scenario
// rejoue exactement la matinee du 24/08 avec quatre modules au lieu de
// sept. Meme energie a couvrir, meme creneau — seule la capacite change,
// donc le SOC exige monte de 30,5 a 35,2 %. Son jumeau ci-dessous, sans le
// champ, prouve que l'absence du helper ne change rien.
node: 'BALANCE',
id: 'bilan-parc-reduit-a-quatre-modules',
label: 'Pack ancien isole : quatre modules au lieu de sept, meme matinee du 24/08 → le SOC exige passe de 27,1 a 30,9 %',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
soc_percent: 21,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
modules_en_service: 4,
},
expect: {
usable: true,
crossingHour: 8,
energyToCoverKwh: 0.82,
requiredSocPct: 30.9,
gridNeeded: true,
prudentRequiredSocPct: 48.2,
deficitTrustworthy: true,
},
},
{
// ======== CHARGE HC MANUELLE (18/09/2026) ========
// Le plancher manuel est pose EN AMONT, dans `coverage()`, et non en ecrivant
// `balance_required_soc` : mesure sur banc, un forcage en aval engage la
// session mais laisse la consigne reseau a 700 W — contacteur ferme, batterie
// a cent watts, et ca A L'AIR de marcher. Ici la cible ET le manque bougent
// ensemble, ce que ce scenario verifie en exigeant les deux.
node: 'BALANCE',
id: 'bilan-plancher-manuel-releve-la-cible-et-le-manque',
label: 'Charge HC manuelle a 85 % : la cible passe de 27,1 a 85 % ET le manque suit (10,34 kWh au lieu de 0,82)',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
soc_percent: 21,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
manual_hc_floor_percent: 85,
},
expect: {
usable: true,
requiredSocPct: 85,
gridNeeded: true,
// 21 -> 85 traverse le genou de 33 % : 12 points a 0,097 puis 52 a 0,169.
gridEnergyNeededKwh: 10.34,
// Le plancher passe par `coverage()`, appelee pour les DEUX profils : la
// cible prudente monte aussi. Consequence assumee — pendant une charge
// manuelle, P12 cesse de descendre la batterie pour la voiture.
prudentRequiredSocPct: 85,
},
},
{
node: 'BALANCE',
id: 'bilan-plancher-manuel-ne-baisse-jamais-la-cible',
label: 'CONTREFACTUEL — plancher manuel a 25 %, sous les 27,1 % calcules : le bilan garde SA cible',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
soc_percent: 21,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
manual_hc_floor_percent: 25,
},
expect: {
// Sens unique, comme les cablages de §38, §39 et §40 : le plancher ne peut
// que RELEVER l'exigence. Un reglage manuel trop bas ne desarme pas le
// bilan, il est simplement sans effet.
usable: true,
requiredSocPct: 27.1,
prudentRequiredSocPct: 37,
},
},
{
node: 'BALANCE',
id: 'bilan-modules-valeur-aberrante-retombe-sur-sept',
label: 'Helper illisible (0 module) : on retombe sur les sept de la mesure plutot que de diviser par zero',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
soc_percent: 21,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
modules_en_service: 0,
},
expect: {
usable: true,
requiredSocPct: 27.1,
prudentRequiredSocPct: 37,
},
},
{
node: 'BALANCE',
id: 'bilan-rejeu-24-08-matin',
label: 'Rejeu réel du 24/08 06:43 : croisement à 08 h, 0,82 kWh à couvrir → SOC requis 27,1 % (courbe plate remesuree 20/09). Le seuil fixe de 30 % tombait juste ce matin-là — par coïncidence, pas par construction',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
soc_percent: 21,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
},
expect: {
usable: true,
crossingHour: 8,
energyToCoverKwh: 0.82,
requiredSocPct: 27.1,
gridNeeded: true,
prudentRequiredSocPct: 37,
deficitTrustworthy: true,
},
},
{
node: 'BALANCE',
id: 'bilan-soc-suffisant-pas-de-reseau',
label: 'Même matinée avec un SOC de 45 % : aucun réseau nécessaire',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
soc_percent: 45,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
},
expect: {
usable: true,
gridNeeded: false,
requiredSocPct: 27.1,
},
},
{
node: 'BALANCE',
id: 'bilan-nuit-sans-croisement',
label: 'Nuit sans production sur tout l\'horizon : le déficit complet est annoncé, pas masqué',
snapshot: {
nowMs: Date.parse('2026-08-24T22:00:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T22:00:00+02:00', 6, {}),
soc_percent: 60,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
},
expect: {
usable: true,
crossingFound: false,
gridNeeded: false,
},
},
{
node: 'BALANCE',
id: 'bilan-prochaine-fenetre-deficit',
label: 'Projection à 15 h : le solaire couvre déjà, mais la nuit qui vient doit être annoncée',
snapshot: {
nowMs: Date.parse('2026-08-24T15:30:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T15:30:00+02:00', 24, {
...PROD_24_08,
15: 3.00, 16: 2.80, 17: 2.60, 18: 2.40, 19: 1.20, 20: 0.20,
// lendemain matin
7: 1.50, 8: 3.00, 9: 5.00,
}),
soc_percent: 95,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
},
expect: {
usable: true,
// fenêtre en cours : le solaire couvre, rien à franchir
gridNeeded: false,
// mais la nuit qui vient doit être chiffrée
nextDeficitFound: true,
nextDeficitStartHour: 19,
nextDeficitEnergyKwh: 12.77,
// Au-dessus de 100 : même pleine, la batterie actuelle ne passe pas la
// nuit sur le profil prudent. C'est le chiffre du dimensionnement.
nextDeficitRequiredSocPct: 101.1,
storageShortfallKwh: 0.17,
},
},
{
node: 'BALANCE',
id: 'bilan-entrees-manquantes',
label: 'Capacité batterie absente : le bilan se déclare inutilisable, les contrôleurs gardent leurs règles',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 6, PROD_24_08),
soc_percent: 21,
},
expect: {
usable: false,
missing: 'usable_capacity_kwh',
},
},
// ================= RÉSERVE D'ORAGE (27/08/2026) =================
// Une coupure réseau n'arrête PAS l'installation : le solaire et la batterie
// continuent, seul le filet de secours disparaît. La réserve à tenir est donc
// la couverture PRUDENTE (p80) de la fenêtre de déficit qui compte — pas une
// nouvelle constante.
{
node: 'BALANCE',
id: 'orage-vigilance-absente',
label: 'Intégration Météo-France non installée : niveau indisponible, aucune activation, mais la réserve reste chiffrée',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
soc_percent: 21,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
storm_alert_level: 'indisponible',
},
expect: {
usable: true,
stormLevel: 'indisponible',
stormActive: false,
// La réserve se calcule quand même : 2,42 kWh pour la matinée du 24/08
// sur le profil prudent, soit SOC 38,1 %.
stormReserveKwh: 2.42,
stormReserveSocPct: 37,
},
},
{
node: 'BALANCE',
id: 'orage-vigilance-jaune-pas-active',
label: 'Vigilance jaune : en dessous du seuil de Météo-France, la réserve est publiée mais pas déclarée d\'actualité',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
soc_percent: 21,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
storm_alert_level: 'Jaune',
},
expect: {
usable: true,
stormLevel: 'jaune',
stormActive: false,
stormReserveKwh: 2.42,
},
},
{
node: 'BALANCE',
id: 'orage-vigilance-orange-deficit-decouvert',
label: 'Vigilance orange à 06:43 avec SOC 21 % : la matinée ne passe pas sans réseau, le manque est chiffré',
snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
soc_percent: 21,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
storm_alert_level: 'Orange',
storm_alert_detail: { 'Orages': 'Orange', 'Vent violent': 'Jaune' },
},
expect: {
usable: true,
stormLevel: 'orange',
stormActive: true,
stormReserveKwh: 2.42,
stormReserveSocPct: 37,
stormCovered: false,
// SOC 21 contre 38,1 requis : il manque 2,57 kWh pour tenir la matinée
// sans réseau.
stormShortfallKwh: 2.58,
},
},
{
node: 'BALANCE',
id: 'orage-vigilance-rouge-surplus-nuit-a-venir',
label: 'Vigilance rouge à 15:30, batterie à 95 % : la fenêtre qui compte est la nuit qui vient, pas celle en cours',
snapshot: {
nowMs: Date.parse('2026-08-24T15:30:00+02:00'),
consumption_p50: P50,
consumption_p80: P80,
horizon: horizonFrom('2026-08-24T15:30:00+02:00', 24, {
...PROD_24_08,
15: 3.00, 16: 2.80, 17: 2.60, 18: 2.40, 19: 1.20, 20: 0.20,
7: 1.50, 8: 3.00, 9: 5.00,
}),
soc_percent: 95,
usable_capacity_kwh: 15,
soc_floor_percent: 22,
storm_alert_level: 'Rouge',
},
expect: {
usable: true,
stormLevel: 'rouge',
stormActive: true,
// Bascule sur la fenêtre de 19 h : 12,77 kWh, soit un SOC requis de
// 107,1 % — même pleine, la batterie actuelle ne passe pas la nuit sans
// réseau. C'est le signal de dimensionnement, il ne doit pas être écrêté.
stormReserveKwh: 12.77,
stormReserveSocPct: 101.1,
stormCovered: false,
stormShortfallKwh: 0.98,
},
},
{
// Nuit du 26-27/09 rejouee avec la prevision de l'epoque (25,1 kWh) : le
// plancher de P12 (tenir jusqu'a la NUIT SUIVANTE, profil prudent) vaut
// 48,4 % — la voiture pouvait puiser de 72 a 48 %, et c'est a peu pres ce qui
// s'est passe (44 %). Calcul juste a zero marge : le reel (22 kWh) a vide la
// batterie a 01:00. La recharge au reseau, elle, ne vise que midi (25,3 %).
node: 'BALANCE',
id: 'bilan-hc-nuit-2709-prevision-forecast-solar',
label: 'Nuit 26-27/09, prevision 25,1 kWh : P12 plancher 48,4 % (nuit suivante), reseau vise midi 25,3 %',
snapshot: snap2709('2026-09-27T01:20:00+02:00', 72, NUIT_2709),
expect: { usable: true, requiredSocPct: 40.2, prudentRequiredSocPct: 48.4, gridNeeded: false,
hcReachActive: true, hcFloorPct: 25.3, hcNightFloorPrudentPct: 48.4 },
},
{
// Meme nuit, journee annoncee sombre (18 kWh, prevision VRM du 27) : le
// plancher de P12 monte a 72,1 %, au-dessus du SOC — la JEEP se charge sur
// le reseau en HC au lieu de vider la batterie. Strategie du proprietaire.
node: 'BALANCE',
id: 'bilan-hc-nuit-2709-journee-sombre-bloque-p12',
label: 'Nuit 26-27/09, journee sombre (18 kWh) : P12 plancher 72,1 % > SOC 72 → voiture sur le reseau HC',
snapshot: snap2709('2026-09-27T01:20:00+02:00', 72, NUIT_2709, SOMBRE),
expect: { usable: true, requiredSocPct: 41.4, prudentRequiredSocPct: 72.1, gridNeeded: false,
hcReachActive: true, hcNightFloorPrudentPct: 72.1 },
},
{
// Midi sombre, batterie a 30 % : tenir jusqu'a 01:24 demande 38,1 % a la fin
// de la fenetre -> session reseau de midi, et le MANQUE suit la cible
// (2,42 kWh = 8,1 points x 0,30 kWh/point). Jamais de session muette.
node: 'BALANCE',
id: 'bilan-hc-midi-sombre-complete-la-batterie',
label: 'Midi sombre, SOC 30 : cible 38,1 % pour tenir jusqu a 01:24, reseau 2,42 kWh (cible et manque ensemble)',
snapshot: snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE),
expect: { usable: true, requiredSocPct: 38.1, gridNeeded: true, gridEnergyNeededKwh: 2.42,
hcReachActive: true, hcFloorPct: 38.1 },
},
{
// Meme instant HORS fenetre HC (pas d'instants transmis) : rien ne change,
// le bilan retombe sur le plancher dur apres le croisement.
node: 'BALANCE',
id: 'bilan-hc-hors-fenetre-inchange',
label: 'Hors fenetre HC : aucune exigence ajoutee (22 %, pas de reseau)',
snapshot: snap2709('2026-09-27T12:30:00+02:00', 30, null, SOMBRE),
expect: { usable: true, requiredSocPct: 22, gridNeeded: false, hcReachActive: false, hcFloorPct: 0 },
}, // ======== JEEP BRANCHEE A MIDI, DEPART 15:30 (28/09/2026) ========
{
node: 'BALANCE',
id: 'bilan-jeep-midi-soleil-solaire-suffit',
label: 'JEEP a 12 %, midi ensoleille (prevision 25,1 kWh), SOC 47 : le solaire couvre, pas de reseau',
// Verifie a la main : 88 % x 8,7 = 7,66 kWh. Plancher au depart 33,5 % ->
// 13,5 points x 0,30 = 4,05 kWh de batterie + 4,96 kWh de surplus net
// 12:30-15:30 = 9,01 kWh >= 7,66.
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 47, MIDI_2709), 12),
expect: { usable: true, vehicleActive: true, vehicleNeedKwh: 7.66, vehicleSolarKwh: 9,
vehicleFloorAtDeparturePct: 33.5, vehicleGridNeeded: false },
},
{
node: 'BALANCE',
id: 'bilan-jeep-midi-sombre-reseau-justifie',
label: 'JEEP a 12 %, midi sombre (18 kWh), SOC 30 : le solaire ne couvre pas avant 15:30 -> reseau justifie',
// La batterie est SOUS son plancher de depart (41,4 %) : il lui manque
// 3,42 kWh, plus que les 2,26 kWh de surplus -> rien pour la voiture.
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 12),
expect: { usable: true, vehicleActive: true, vehicleSolarKwh: 0, vehicleReachableKwh: 7.66,
vehicleGridNeeded: true },
},
{
node: 'BALANCE',
id: 'bilan-jeep-midi-sans-depart-regle-muette',
label: 'Jour sans depart (lundi, jeudi, dimanche) : la regle se tait, la HC de 01:24 finira la charge',
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 12, { vehicle_departure_ms: null }),
expect: { usable: true, vehicleActive: false, vehicleGridNeeded: false },
},
{
node: 'BALANCE',
id: 'bilan-jeep-midi-niveau-perime-regle-muette',
label: 'Niveau saisi avant ce branchement : on ne devine pas le besoin, la regle se tait',
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 12, { vehicle_level_valid: false }),
expect: { usable: true, vehicleActive: false, vehicleGridNeeded: false },
},
{
node: 'BALANCE',
id: 'bilan-jeep-midi-deja-chargee-par-le-solaire',
label: 'Saisie 12 % puis 7,5 kWh deja delivres : besoin residuel 0,16 kWh, sous le seuil -> pas de reseau',
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 12, { vehicle_delivered_since_level_kwh: 7.5 }),
expect: { usable: true, vehicleActive: true, vehicleNeedKwh: 0.16, vehicleGridNeeded: false },
},
{
node: 'BALANCE',
id: 'bilan-jeep-hors-fenetre-hc-regle-muette',
label: 'Hors fenetre HC (15:00) : la regle ne s engage jamais hors HC',
snapshot: jeep(snap2709('2026-09-27T15:00:00+02:00', 30, null, SOMBRE), 12),
expect: { usable: true, vehicleActive: false, vehicleGridNeeded: false },
},
// ======== LES TROIS VEHICULES (28/09/2026) ========
{
node: 'BALANCE',
id: 'bilan-vehicule-capacite-inconnue-regle-muette',
label: 'Moto ZERO, capacite non mesuree (0) : on ne devine pas son besoin, la regle se tait',
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 12, { vehicle_capacity_kwh: 0 }),
expect: { usable: true, vehicleActive: false, vehicleGridNeeded: false },
},
{
node: 'BALANCE',
id: 'bilan-vehicule-puissance-bornee',
label: 'Chargeur embarque a 3,7 kW, 3 h avant le depart : le vehicule ne peut pas prendre plus de 11,1 kWh',
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 0, { vehicle_capacity_kwh: 15 }),
expect: { usable: true, vehicleActive: true, vehicleNeedKwh: 15, vehicleReachableKwh: 11.1, vehicleGridNeeded: true },
},
{
node: 'BALANCE',
id: 'bilan-vehicule-puissance-inconnue-non-bornee',
label: 'Puissance de charge inconnue (0) : on ne borne pas, tout le besoin est compte',
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 0, { vehicle_capacity_kwh: 15, vehicle_charger_kw: 0 }),
expect: { usable: true, vehicleActive: true, vehicleReachableKwh: 15, vehicleGridNeeded: true },
},
];