Chantier etapes 5b et 5c : recalcul quotidien des profils et fenetre de deficit suivante
5c — BALANCE ne repondait qu'a "combien jusqu'a ce que le solaire prenne le relais". A 15 h la reponse est zero : exacte mais inutile, le deficit qui approche etant celui de la nuit. Ajout de la prochaine traversee deficitaire (heure de debut, energie, SOC a atteindre avant). Ce SOC n'est PAS ecrete a 100 % : au-dela, meme une batterie pleine ne passe pas, et le manque est publie comme storage_shortfall_kwh — c'est le chiffre du dimensionnement. 5b — Workflow "EVCS Energy Profiles Daily V1" (N6UaB4ZozsbRPrFb), actif, 03:30 : lit /api/history/period sur 30 jours glissants et republie les quatre CSV (conso p50 et p80, enveloppe de production, taux de censure). CONSUMPTION_PROFILE devient DAILY_PROFILES et sait desormais decoder la reponse brute de l'API en plus des series deja pretes. Piege trouve : sans end_time, /api/history/period ne renvoie qu'UNE journee, silencieusement. Mes premieres mesures "sur 3 jours" n'en couvraient qu'une. Validation croisee : profils calcules depuis l'API REST (etats bruts, 11 j) contre profils calcules depuis les statistiques long terme (28 j) — 0,20 kWh/h d'ecart absolu moyen, par deux chemins de donnees independants. BUILD_HA_CALLS_PROFILES refuse d'ecraser les profils en place si l'historique ne couvre pas les 24 heures. Meme garantie de perimetre que le bilan. Suite a 27 scenarios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
@@ -140,6 +140,59 @@ function coverage(slots) {
|
||||
const typical = coverage(buildSlots(consumptionP50));
|
||||
const prudent = coverage(buildSlots(consumptionP80));
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// PROCHAINE FENÊTRE DE DÉFICIT (ajout du 24/08/2026)
|
||||
//
|
||||
// Le calcul ci-dessus répond à « combien d'énergie jusqu'à ce que le solaire
|
||||
// prenne le relais ». À 15 h, le solaire a déjà pris le relais : la réponse est
|
||||
// zéro et le SOC requis retombe sur le plancher. C'est exact mais inutile à
|
||||
// cette heure-là, car le déficit qui approche est celui de la nuit.
|
||||
//
|
||||
// On cherche donc AUSSI la prochaine fenêtre de déficit après le surplus en
|
||||
// cours : son heure de début, son énergie, et le SOC qu'il faudra avoir atteint
|
||||
// à ce moment-là. C'est cette grandeur qui remplace la cible de nuit fixe, et
|
||||
// qui dit combien de surplus il faut avoir stocké avant le coucher du soleil.
|
||||
//
|
||||
// Profil PRUDENT : contrairement à la décision réseau immédiate, cette
|
||||
// projection engage plusieurs heures et n'est pas révisable à volonté — la nuit
|
||||
// tombée, il n'y a plus de solaire pour rattraper une sous-estimation.
|
||||
// ---------------------------------------------------------------------
|
||||
function nextDeficitWindow(slots) {
|
||||
const firstSurplus = slots.findIndex(s => s.netKwh > 0);
|
||||
if (firstSurplus < 0) return { found: false, reason: 'deficit_en_cours' };
|
||||
|
||||
const start = slots.findIndex((s, i) => i > firstSurplus && s.netKwh < 0);
|
||||
if (start < 0) return { found: false, reason: 'aucun_deficit_sur_horizon' };
|
||||
|
||||
let end = slots.findIndex((s, i) => i > start && s.netKwh > 0);
|
||||
if (end < 0) end = slots.length;
|
||||
|
||||
const energyKwh = slots.slice(start, end)
|
||||
.reduce((sum, s) => sum + Math.max(0, -s.netKwh), 0);
|
||||
|
||||
// Surplus encore récupérable d'ici là : de quoi remplir, ou pas.
|
||||
const surplusBeforeKwh = slots.slice(0, start)
|
||||
.reduce((sum, s) => sum + Math.max(0, s.netKwh), 0);
|
||||
|
||||
return {
|
||||
found: true,
|
||||
startHour: slots[start].hour,
|
||||
startMs: slots[start].startMs,
|
||||
hoursFromNow: start,
|
||||
endHour: end < slots.length ? slots[end].hour : null,
|
||||
complete: end < slots.length,
|
||||
energyKwh,
|
||||
// NON écrêté à 100 volontairement : une valeur au-dessus de 100 signifie
|
||||
// que même une batterie pleine ne suffit pas à traverser la fenêtre. C'est
|
||||
// exactement le signal de sous-dimensionnement du stockage — l'écrêter le
|
||||
// masquerait.
|
||||
requiredSocAtStartPct: socFloorPct + (energyKwh / usableCapacityKwh) * 100,
|
||||
surplusBeforeKwh,
|
||||
};
|
||||
}
|
||||
|
||||
const nextDeficit = nextDeficitWindow(prudent.slots);
|
||||
|
||||
const slots = typical.slots;
|
||||
const crossingFound = typical.found;
|
||||
const crossingIndex = typical.crossingIndex;
|
||||
@@ -199,6 +252,28 @@ return [{
|
||||
},
|
||||
},
|
||||
|
||||
// Ce qu'il faudra avoir stocké avant la prochaine traversée déficitaire.
|
||||
nextDeficit: nextDeficit.found ? {
|
||||
found: true,
|
||||
startHour: nextDeficit.startHour,
|
||||
hoursFromNow: nextDeficit.hoursFromNow,
|
||||
endHour: nextDeficit.endHour,
|
||||
complete: nextDeficit.complete,
|
||||
energyKwh: round2(nextDeficit.energyKwh),
|
||||
requiredSocAtStartPct: round1(nextDeficit.requiredSocAtStartPct),
|
||||
surplusBeforeKwh: round2(nextDeficit.surplusBeforeKwh),
|
||||
// Franchissable avec le stockage actuel, batterie pleine ?
|
||||
feasibleWithCurrentStorage: nextDeficit.requiredSocAtStartPct <= 100,
|
||||
// Si non : capacité utile manquante. C'est le chiffre du dimensionnement.
|
||||
storageShortfallKwh: Math.max(
|
||||
0,
|
||||
((nextDeficit.requiredSocAtStartPct - 100) / 100) * usableCapacityKwh
|
||||
),
|
||||
reachable: nextDeficit.surplusBeforeKwh >= 0
|
||||
&& (socPct + (nextDeficit.surplusBeforeKwh / usableCapacityKwh) * 100)
|
||||
>= nextDeficit.requiredSocAtStartPct,
|
||||
} : { found: false, reason: nextDeficit.reason },
|
||||
|
||||
vehicle: {
|
||||
surplusAvailableKwh: round2(surplusAfterCrossingKwh),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user