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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user