Bilan : la nuit prepare jusqu'a la nuit suivante (la regle du 08/10 prenait le creux de midi pour le soir)

This commit is contained in:
Tallagrand Thierry
2026-10-09 01:41:08 +02:00
parent b7448b72c4
commit 7969318185
6 changed files with 80 additions and 41 deletions

View File

@@ -473,41 +473,50 @@ const hcMaxCharge = hcMiddayWindow && ecsNuitAujourdhui;
// « tenir jusqu'a midi »).
// ---------------------------------------------------------------------
const hcNightWindow = hcReachActive && !hcMiddayWindow;
// ⚠️ CORRIGE LE 09/10 : la premiere version (08/10) visait « le debut de la
// traversee du soir » = la premiere fenetre de deficit apres le premier surplus.
// Nuit du 08-09/10 (VRM 14,7 kWh) : un surplus a 11 h puis un creux a 12 h ->
// « traversee » a 12 h, 22 % requis, regle muette alors que la journee etait
// faible. L'objectif est maintenant sans ambiguite : de la fin de la fenetre de
// nuit au debut de la NUIT SUIVANTE (01:24), la batterie ne doit JAMAIS passer
// sous le plancher, au profil habituel, solaire ecrete a 100 %. La HC de midi
// n'est pas comptee (prudence : elle est bornee par la maison, et ce que la nuit
// charge au meme prix ne lui coute rien).
let nightEvening = { active: false };
if (hcNightWindow) {
const ndEve = nextDeficitWindow(slotsP50);
if (ndEve.found && Number.isFinite(num(ndEve.startMs, NaN)) && ndEve.startMs > hcWindowEndMs) {
const socAtEvening = (s0) => {
let soc = s0;
for (const sl of slotsP50) {
const debut = num(sl.startMs, NaN);
if (!Number.isFinite(debut)) continue;
const a = Math.max(debut, hcWindowEndMs);
const b = Math.min(debut + 3600000, ndEve.startMs);
if (b <= a) continue;
const net = sl.netKwh * (b - a) / 3600000;
soc = net >= 0 ? Math.min(100, socAfterEnergy(soc, net))
: Math.max(socFloorPct, socForEnergy(Math.max(0, energyBetweenSoc(0, soc) + net), 0));
}
return soc;
};
const requis = ndEve.requiredSocAtStartPct;
const base = Math.max(socFloorPct, hcFloorReachPct);
const objectif = Math.min(requis, socAtEvening(100)) - 0.05;
let floor = base;
if (socAtEvening(base) < objectif) {
let lo = base, hi = 100;
for (let k = 0; k < 30; k++) { const m = (lo + hi) / 2; if (socAtEvening(m) >= objectif) hi = m; else lo = m; }
floor = hi;
if (hcNightWindow && Number.isFinite(nextNightHcStartMs) && nextNightHcStartMs > hcWindowEndMs) {
const capKwh = energyBetweenSoc(socFloorPct, 100);
// Energie minimale au-dessus du plancher sur le parcours, depuis e0 en fin de nuit.
const minAuDessus = (e0) => {
let e = e0, mini = e0;
for (const sl of slotsP50) {
const debut = num(sl.startMs, NaN);
if (!Number.isFinite(debut)) continue;
const a = Math.max(debut, hcWindowEndMs);
const b = Math.min(debut + 3600000, nextNightHcStartMs);
if (b <= a) continue;
e = Math.min(capKwh, e + sl.netKwh * (b - a) / 3600000);
if (e < mini) mini = e;
}
nightEvening = {
active: floor > base + 0.05,
floorPct: round1(floor),
eveningRequiredPct: round1(requis),
eveningProjectedPct: round1(socAtEvening(base)),
eveningStartHour: ndEve.startHour,
};
return mini;
};
const base = Math.max(socFloorPct, hcFloorReachPct);
const eBase = energyBetweenSoc(socFloorPct, base);
// Hors d'atteinte meme pleine : on vise le mieux possible, sans gaspiller.
const objectif = Math.min(0, minAuDessus(capKwh)) - 0.01;
let e0 = eBase;
if (minAuDessus(eBase) < objectif) {
let lo = eBase, hi = capKwh;
for (let k = 0; k < 40; k++) { const m = (lo + hi) / 2; if (minAuDessus(m) >= objectif) hi = m; else lo = m; }
e0 = hi;
}
const floor = socAfterEnergy(socFloorPct, e0);
nightEvening = {
active: floor > base + 0.05,
floorPct: round1(Math.min(100, floor)),
// Sans relever : energie qui manquerait sous le plancher avant la nuit suivante.
shortfallKwh: round2(Math.max(0, -minAuDessus(eBase))),
untilMs: nextNightHcStartMs,
};
}
const hcFloorTypicalPct = hcMaxCharge ? 100
: (nightEvening.active ? Math.max(hcFloorReachPct, nightEvening.floorPct) : hcFloorReachPct);

View File

@@ -162,7 +162,7 @@ const reason = balance.crossing.found
const hc = balance.hcReach || {};
const ne = hc.nightEvening || {};
const hcText = hc.active && ne.active
? ` Journée faible annoncée : la nuit charge à ${ne.floorPct}% (batterie prévue à ${ne.eveningProjectedPct}% à ${ne.eveningStartHour} h pour ${ne.eveningRequiredPct}% requis ; tenir jusqu'à midi demandait ${hc.floorReachPct}%).`
? ` Journée faible annoncée : la nuit charge à ${ne.floorPct}% (sinon il manquerait ${ne.shortfallKwh} kWh avant la nuit prochaine ; tenir jusqu'à midi demandait ${hc.floorReachPct}%).`
: hc.active && hc.maxCharge
? ` Jour faible : charge maximale en HC de midi (tenir jusqu'à ${new Date(hc.nextHcStartMs).toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' })} demandait ${hc.floorReachPct}%).`
: hc.active && hc.raisesRequirement