765 lines
36 KiB
JavaScript
765 lines
36 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);
|
|
}
|
|
|
|
const NUIT_HC_2809 = { night_hc_start_ms: Date.parse('2026-09-28T01:24:00+02:00'),
|
|
night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00') };
|
|
// Lendemain ensoleille : production du 28/09 x 1,5 (~49 kWh), le reste inchange.
|
|
function soleil2809(snapshot) {
|
|
snapshot.horizon = snapshot.horizon.map(sl =>
|
|
new Date(sl.startMs).toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' }) === '2026-09-28'
|
|
? { ...sl, expectedKwh: FS_2809[sl.hour] * 1.5 } : sl);
|
|
return snapshot;
|
|
}
|
|
|
|
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-batterie-non-comptee',
|
|
label: 'JEEP a 12 %, midi ensoleille (prevision 25,1 kWh), SOC 47 : seul le surplus net compte (4,96 < 7,66) -> reseau',
|
|
// Regle du 06/10/2026 : la batterie au-dessus du plancher n'est plus
|
|
// comptee (le controleur ne la donne pas a la voiture sous 90 %).
|
|
// Verifie a la main : 88 % x 8,7 = 7,66 kWh ; plancher au depart 33,5 %
|
|
// < SOC 47 -> aucun manque batterie ; surplus net 12:30-15:30 = 4,96 kWh
|
|
// (avant le 06/10 : + 4,05 kWh de batterie = 9,01, pas de reseau).
|
|
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 47, MIDI_2709), 12),
|
|
expect: { usable: true, vehicleActive: true, vehicleNeedKwh: 7.66, vehicleSolarKwh: 4.96,
|
|
vehicleFloorAtDeparturePct: 33.5, vehicleGridNeeded: true },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-jeep-midi-batterie-haute-assistance-comptee',
|
|
label: 'JEEP a 60 %, midi ensoleille, SOC 95 : surplus net + batterie au-dessus de 90 % (assistance) couvrent -> pas de reseau',
|
|
// Besoin 40 % x 8,7 = 3,48 kWh ; surplus net 4,96 + (95 - 90) x 0,30 = 1,5
|
|
// de batterie d'assistance = 6,46 kWh >= 3,48.
|
|
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 95, MIDI_2709), 60),
|
|
expect: { usable: true, vehicleActive: true, vehicleNeedKwh: 3.48, vehicleSolarKwh: 6.46,
|
|
vehicleGridNeeded: false },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-jeep-midi-0610-surplus-insuffisant',
|
|
label: 'Cas du 06/10 13:12 rejoue : SOC 57 bien au-dessus du plancher, mais surplus net insuffisant -> reseau',
|
|
// Meme construction : JEEP a 56 %, besoin 44 % x 8,7 = 3,83 kWh ; la
|
|
// batterie au-dessus du plancher ne compte pas, seul le surplus net.
|
|
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 57, MIDI_2709, SOMBRE), 56),
|
|
expect: { usable: true, vehicleActive: true, vehicleNeedKwh: 3.83, vehicleSolarKwh: 2.26,
|
|
vehicleGridNeeded: true },
|
|
},
|
|
{
|
|
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 },
|
|
},
|
|
// ======== CHAUFFE-EAU EN HC DE NUIT LES JOURS FAIBLES (07/10/2026) ========
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-ecs-jour-sombre-eau-la-nuit',
|
|
label: 'Matin 07:30, journee sombre (18 kWh), SOC 40 : la batterie n arrive pas au requis du soir -> chauffe-eau en HC de nuit',
|
|
snapshot: Object.assign(snap2709('2026-09-27T07:30:00+02:00', 40, null, SOMBRE),
|
|
{ night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00'), ecs_nuit_jour: '2026-09-26|off|x' }),
|
|
expect: { usable: true, ecsDue: true, ecsDecided: true, ecsOn: true },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-ecs-jour-ensoleille-eau-au-solaire',
|
|
label: 'Matin 07:30, journee normale (25,1 kWh), SOC 70 : la batterie atteint le requis du soir -> chauffe-eau au solaire',
|
|
snapshot: Object.assign(snap2709('2026-09-27T07:30:00+02:00', 70, null),
|
|
{ night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00'), ecs_nuit_jour: '2026-09-26|on|x' }),
|
|
expect: { usable: true, ecsDue: true, ecsDecided: true, ecsOn: false },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-ecs-decision-figee-dans-la-journee',
|
|
label: 'Deja decide aujourd hui : pas de nouvelle decision (sinon retirer le chauffe-eau changerait la projection -> oscillateur)',
|
|
snapshot: Object.assign(snap2709('2026-09-27T13:00:00+02:00', 40, null, SOMBRE),
|
|
{ night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00'), ecs_nuit_jour: '2026-09-27|on|x' }),
|
|
expect: { usable: true, ecsDue: false, ecsDecided: false },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-ecs-pas-de-decision-avant-la-fin-de-la-nuit',
|
|
label: '00:30, nouvelle date mais fenetre HC de nuit pas encore passee : la decision de la veille vaut encore',
|
|
snapshot: Object.assign(snap2709('2026-09-27T00:30:00+02:00', 40, null, SOMBRE),
|
|
{ night_hc_end_ms: Date.parse('2026-09-27T07:24:00+02:00'), ecs_nuit_jour: '2026-09-26|on|x' }),
|
|
expect: { usable: true, ecsDue: false, ecsDecided: false },
|
|
},
|
|
// ======== JOUR FAIBLE : REPRISE DU SOIR ET PREVISION RECALEE (07/10/2026) ========
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-midi-reference-sans-correction',
|
|
label: 'Reference : HC de midi 12:30, SOC 31, journee sombre — plancher « tenir jusqu a 01:24 »',
|
|
snapshot: snap2709('2026-09-27T12:30:00+02:00', 31, MIDI_2709, SOMBRE),
|
|
expect: { usable: true, hcReachActive: true, hcFloorPct: 38.1 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-midi-jour-faible-reprise-du-soir',
|
|
label: 'Meme midi, jour faible : 2,1 kWh de chauffe-eau de 16 h a 20 h comptes -> plancher plus haut',
|
|
snapshot: Object.assign(snap2709('2026-09-27T12:30:00+02:00', 31, MIDI_2709, SOMBRE),
|
|
{ ecs_nuit_jour: '2026-09-27|on|x', ecs_reprise_soir_kwh: '2.1' }),
|
|
// Verifie a la main : 2,1 kWh / 0,30 kWh par point = +7,0 points (38,1 -> 45,1).
|
|
// Depuis le 08/10, un jour faible a midi vise la charge maximale (100 %) ;
|
|
// le plancher « tenir jusqu'a 01:24 » reste calcule et publie a cote.
|
|
expect: { usable: true, hcReachActive: true, ecsRepriseKwh: 2.1, hcFloorReachPct: 45.1,
|
|
hcMaxCharge: true, hcFloorPct: 100 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-midi-jour-normal-pas-de-charge-max',
|
|
label: 'Meme midi, jour NON faible : la cible reste « tenir jusqu a 01:24 », pas de charge maximale',
|
|
snapshot: Object.assign(snap2709('2026-09-27T12:30:00+02:00', 31, MIDI_2709, SOMBRE),
|
|
{ ecs_nuit_jour: '2026-09-27|off|x' }),
|
|
expect: { usable: true, hcReachActive: true, hcMaxCharge: false, hcFloorPct: 38.1 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-midi-jour-faible-charge-max-engage-et-finance',
|
|
label: 'Jour faible a midi, SOC 31 : cible 100 % -> session engagee ET manque chiffre (69 points x 0,30)',
|
|
snapshot: Object.assign(snap2709('2026-09-27T12:30:00+02:00', 31, MIDI_2709, SOMBRE),
|
|
{ ecs_nuit_jour: '2026-09-27|on|x' }),
|
|
// Verifie a la main : (100 - 31) x 0,30 = 20,7 kWh.
|
|
expect: { usable: true, hcMaxCharge: true, requiredSocPct: 100, gridNeeded: true, gridEnergyNeededKwh: 20.7 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-nuit-jour-faible-pas-de-charge-max',
|
|
label: 'Fenetre HC de NUIT, decision jour faible du jour : la nuit garde « tenir jusqu a midi »',
|
|
snapshot: Object.assign(snap2709('2026-09-27T01:20:00+02:00', 72, NUIT_2709),
|
|
{ ecs_nuit_jour: '2026-09-27|on|x' }),
|
|
expect: { usable: true, hcReachActive: true, hcMaxCharge: false, hcFloorPct: 25.3 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-midi-prevision-recalee-sur-la-matinee',
|
|
label: 'Meme midi, matinee a 60 % de la prevision : les heures restantes du jour x 0,6 -> plancher plus haut',
|
|
snapshot: Object.assign(snap2709('2026-09-27T12:30:00+02:00', 31, MIDI_2709, SOMBRE), {
|
|
forecast_today_csv: FS_2709.map((v) => (v * SOMBRE).toFixed(3)).join(','),
|
|
production_jour_kwh: String(Math.round(FS_2709.slice(0, 12).reduce((a, b) => a + b, 0) * SOMBRE * 0.6 * 100 + FS_2709[12] * SOMBRE * 0.5 * 0.6 * 100) / 100),
|
|
production_jour_statut: 'complet' }),
|
|
expect: { usable: true, hcReachActive: true, prodRecalFactor: 0.6, hcFloorPct: 49.4 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-prevision-jamais-recalee-a-la-hausse',
|
|
label: 'Matinee meilleure que prevu : facteur 1, on ne promet rien de plus pour l apres-midi',
|
|
snapshot: Object.assign(snap2709('2026-09-27T12:30:00+02:00', 31, MIDI_2709, SOMBRE), {
|
|
forecast_today_csv: FS_2709.map((v) => (v * SOMBRE).toFixed(3)).join(','),
|
|
production_jour_kwh: '40', production_jour_statut: 'complet' }),
|
|
expect: { usable: true, prodRecalFactor: 1 },
|
|
},
|
|
// ======== 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 },
|
|
},
|
|
// ======== LA NUIT PREVUE PAR LA SOIREE (28/09/2026) ========
|
|
// Soir du 28/09 : soiree 6,27 kWh -> facteur 1,092 sur le profil de 21:30 a
|
|
// 09:00. Meme snapshot avec et sans facteur : seule la consommation de nuit
|
|
// bouge, donc le SOC requis monte.
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-nuit-soiree-reference-sans-facteur',
|
|
label: 'Reference : 23:00, SOC 52, sans facteur de nuit',
|
|
snapshot: snap2709('2026-09-27T23:00:00+02:00', 52, null),
|
|
expect: { usable: true, nightModelActive: false, nightFactor: 1, energyToCoverKwh: 7.77, requiredSocPct: 47.9 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-nuit-soiree-chargee-releve-le-requis',
|
|
label: 'Soiree chargee (facteur 1,092) : la nuit est plus lourde, le SOC requis monte',
|
|
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), {
|
|
night_house_factor: 1.092, night_house_forecast_kwh: '11.59',
|
|
night_house_start_ms: Date.parse('2026-09-27T21:30:00+02:00'), night_house_end_ms: Date.parse('2026-09-28T09:00:00+02:00'),
|
|
night_house_computed_ms: Date.parse('2026-09-27T22:57:00+02:00') }),
|
|
// Verifie a la main : profil 23:00->09:00 = 8,0 kWh, x 0,092 = +0,74 (0,71
|
|
// apres l'heure ou le soleil revient) ; +2,4 points = 0,71 / 0,30. Le
|
|
// prudent (p80) est deja au-dessus du p50 recale : il ne bouge pas.
|
|
expect: { usable: true, nightModelActive: true, nightFactor: 1.092, energyToCoverKwh: 8.48,
|
|
requiredSocPct: 50.3, prudentRequiredSocPct: 56.7 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-nuit-soiree-facteur-perime',
|
|
label: 'Facteur calcule il y a 2 h : on ne l applique pas',
|
|
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), {
|
|
night_house_factor: 1.092,
|
|
night_house_start_ms: Date.parse('2026-09-27T21:30:00+02:00'), night_house_end_ms: Date.parse('2026-09-28T09:00:00+02:00'),
|
|
night_house_computed_ms: Date.parse('2026-09-27T21:00:00+02:00') }),
|
|
expect: { usable: true, nightModelActive: false, nightFactor: 1 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-nuit-soiree-facteur-aberrant-borne',
|
|
label: 'Facteur 3 (mesure fausse) : borne a 1,45, l extreme observe',
|
|
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), {
|
|
night_house_factor: 3,
|
|
night_house_start_ms: Date.parse('2026-09-27T21:30:00+02:00'), night_house_end_ms: Date.parse('2026-09-28T09:00:00+02:00'),
|
|
night_house_computed_ms: Date.parse('2026-09-27T22:57:00+02:00') }),
|
|
expect: { usable: true, nightModelActive: true, nightFactor: 1.45, requiredSocPct: 59.8, prudentRequiredSocPct: 62.2 },
|
|
},
|
|
// ======== CE QUE LA SESSION DE NUIT ACHETERA (29/09/2026) ========
|
|
// Nuit du 28-29/09 : « 0,14 kWh a prendre sur EDF » annonces, 4,6 importes :
|
|
// la session engagee fait passer TOUTE la maison sur le reseau de 01:24 a
|
|
// 07:24. Verifie a la main sur le profil p50 (pas de soleil avant 8 h) :
|
|
// 0,6 x 0,80 + 0,70 + 0,60 + 0,70 + 0,70 + 0,70 + 0,4 x 0,70 = 4,16 kWh.
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-nuit-maison-sur-le-reseau',
|
|
label: '23:00, fenetre HC de nuit 01:24-07:24 a venir : la maison y prendra 4,16 kWh au reseau',
|
|
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), {
|
|
night_hc_start_ms: Date.parse('2026-09-28T01:24:00+02:00'), night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00') }),
|
|
expect: { usable: true, nightHcActive: true, nightHcHouseKwh: 4.16, requiredSocPct: 47.9 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-nuit-maison-recalee-sur-la-soiree',
|
|
label: 'Soiree chargee (facteur 1,092) : la maison en HC de nuit monte a 4,53 kWh',
|
|
// Tranches recalees puis arrondies : 0,87 / 0,76 / 0,66 -> 0,6 x 0,87 +
|
|
// 0,76 + 0,66 + 3 x 0,76 + 0,4 x 0,76 = 4,53.
|
|
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), {
|
|
night_house_factor: 1.092,
|
|
night_house_start_ms: Date.parse('2026-09-27T21:30:00+02:00'), night_house_end_ms: Date.parse('2026-09-28T09:00:00+02:00'),
|
|
night_house_computed_ms: Date.parse('2026-09-27T22:57:00+02:00'),
|
|
night_hc_start_ms: Date.parse('2026-09-28T01:24:00+02:00'), night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00') }),
|
|
expect: { usable: true, nightModelActive: true, nightHcActive: true, nightHcHouseKwh: 4.53 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-nuit-fenetre-ouverte-depuis-maintenant',
|
|
label: '03:30, fenetre ouverte : seul le reste de la fenetre compte (2,68 kWh)',
|
|
// La tranche de 03 h n'est comptee que pour sa demi-heure restante :
|
|
// 0,5 x 0,60 + 0,70 + 0,70 + 0,70 + 0,4 x 0,70 = 2,68.
|
|
snapshot: Object.assign(snap2709('2026-09-28T03:30:00+02:00', 45, null), {
|
|
night_hc_start_ms: Date.parse('2026-09-28T01:24:00+02:00'), night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00') }),
|
|
expect: { usable: true, nightHcActive: true, nightHcHouseKwh: 2.68 },
|
|
},
|
|
// ======== BUDGET VOITURE DE LA NUIT SANS EDF (30/09/2026) ========
|
|
// Decisions du proprietaire : profil prudent (p80), plancher 22 %, jusqu'a la
|
|
// nuit suivante. Attendus calcules par un script Python independant (meme
|
|
// trajectoire, creux a 9 h). Lendemain ensoleille = production du 28/09 x 1,5.
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-budget-voiture-nuit-soleil',
|
|
label: '01:25, SOC 57, lendemain ensoleille : budget borne par le plancher prudent 44,5 % -> (57 - 44,5) x 0,30 = 3,75 kWh (creux seul : 4,21, avant le 04/10)',
|
|
snapshot: soleil2809(Object.assign(snap2709('2026-09-28T01:25:00+02:00', 57, null), NUIT_HC_2809)),
|
|
expect: { usable: true, nightVehicleBudgetActive: true, nightVehicleBudgetKwh: 3.75, prudentRequiredSocPct: 44.5 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-budget-voiture-nuit-soc-bas',
|
|
label: '01:25, SOC 45 : borne par le plancher prudent -> (45 - 44,5) x 0,30 = 0,15 kWh (creux seul : 0,61, avant le 04/10)',
|
|
snapshot: soleil2809(Object.assign(snap2709('2026-09-28T01:25:00+02:00', 45, null), NUIT_HC_2809)),
|
|
expect: { usable: true, nightVehicleBudgetActive: true, nightVehicleBudgetKwh: 0.15, prudentRequiredSocPct: 44.5 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-budget-voiture-nuit-lendemain-sombre',
|
|
label: 'Lendemain sombre (27-28/09) : la batterie ne tiendrait pas la nuit suivante, budget nul',
|
|
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), NUIT_HC_2809),
|
|
expect: { usable: true, nightVehicleBudgetActive: true, nightVehicleBudgetKwh: 0 },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-budget-voiture-fenetre-inconnue',
|
|
label: 'Fenetre HC de nuit non transmise : pas de budget (le controleur ne donne rien)',
|
|
snapshot: snap2709('2026-09-28T01:25:00+02:00', 57, null),
|
|
expect: { usable: true, nightVehicleBudgetActive: false, nightVehicleBudgetKwh: null },
|
|
},
|
|
{
|
|
node: 'BALANCE',
|
|
id: 'bilan-hc-nuit-fenetre-inconnue',
|
|
label: 'Fenetre HC de nuit non transmise : rien n est annonce',
|
|
snapshot: snap2709('2026-09-27T23:00:00+02:00', 52, null),
|
|
expect: { usable: true, nightHcActive: false, nightHcHouseKwh: null },
|
|
},
|
|
];
|