La prevision se juge sur la date qu elle decrit, plus sur son age

Nuit du 07 au 08/09 : 32 calculs du bilan sur 120 en statut degrade, tous entre
00 h et 03 h, avec « aucun croisement PV/conso sur l'horizon, 47,6 kWh a
couvrir ». Le controleur, qui exige statut=ok, retombait sur le palier de 35 % —
d'ou une cible du matin oscillant entre 35, 78,8 et 85 % au lieu de decroitre.

Cause immediate : plus aucune publication de prevision de 23:45 a 04:30. Le
noeud d'appel porte neverError, donc les executions restaient en success ; la
reponse etait « socket hang up », et le noeud d'ecriture vers Home Assistant
rendait 0 item la nuit contre 1 en journee — le .17 etait lui aussi injoignable,
si bien que le message d'echec ne pouvait meme pas etre publie.

Mais le defaut qu'on controle est ailleurs : les courbes sont publiees sans leur
date, et PARSE_FORECAST prend days[0] pour « aujourd'hui ». Vrai a l'instant de
la publication, faux des qu'on passe minuit sans rafraichir. A 00:45 le bilan a
donc lu comme journee en cours celle du 7, dont toutes les heures etaient
passees.

Une courbe est desormais utilisable si elle DECRIT le jour dont on a besoin, pas
si elle est recente : une prevision publiee a 23:45 pour le 8 reste exacte a
03:00, la nuit il n'y a rien a reactualiser. La date part dans
evcs_balance_forecast_day0, et RESOURCE_ESTIMATE distingue trois cas — a jour,
decalee d'un jour (la courbe du lendemain devient celle du jour, l'enveloppe
mesuree reprend la main pour demain), perimee. Helper absent : ancienne garde
d'age, comportement inchange.

Le rapport de nuit signale un trou de plus de deux heures dans les
publications. Il lit la base sur l'autre NAS, seul temoin qui survit quand le
.17 est injoignable. Verifie sur la nuit ecoulee : 285 minutes.

Tests 44 -> 48. Les quatre scenarios partagent la meme heure — 00:45, l'instant
du decrochage — et les memes courbes : seule la date change. Quatre mutations,
chacune sur un jeu different, chaque scenario ayant la sienne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-08 09:30:35 +02:00
parent c42b29c76f
commit 8d40e0d446
10 changed files with 286 additions and 8 deletions

View File

@@ -31,6 +31,15 @@ if (!parsed.ok) {
return calls;
}
// La DATE que decrit la courbe, et non l'heure a laquelle on l'a recue.
// Sans elle, « today » veut dire « le premier jour renvoye par l'API au moment
// de la publication » — ce qui devient faux des qu'on passe minuit sans
// rafraichir. Nuit du 07 au 08/09 : plus aucune publication de 23:45 a 04:30,
// et le bilan a lu comme « aujourd'hui » la courbe du 7, dont toutes les heures
// etaient passees. Il en a conclu « aucun croisement, 47,6 kWh a couvrir, SOC
// requis 100 % », s'est declare degrade, et le controleur est retombe sur le
// palier de 35 % pendant quatre heures.
setText('input_text.evcs_balance_forecast_day0', parsed.today.date);
setText('input_text.evcs_balance_forecast_today', parsed.today.csv);
if (parsed.tomorrow) {
setText('input_text.evcs_balance_forecast_tomorrow', parsed.tomorrow.csv);

View File

@@ -11,6 +11,7 @@
"envelope_csv": states("input_text.evcs_balance_envelope"),
"censorship_rate_csv": states("input_text.evcs_balance_censorship_rate"),
"forecast_day0": states("input_text.evcs_balance_forecast_day0"),
"forecast_today_csv": states("input_text.evcs_balance_forecast_today"),
"forecast_tomorrow_csv": states("input_text.evcs_balance_forecast_tomorrow"),
"forecast_updated": states("input_text.evcs_balance_forecast_updated"),

View File

@@ -111,7 +111,6 @@ function read24(arrayKey, csvKey) {
const hourlyToday = read24('forecast_today_kwh', 'forecast_today_csv');
const hourlyTomorrow = read24('forecast_tomorrow_kwh', 'forecast_tomorrow_csv');
const hourlyAvailable = !!hourlyToday && forecastFresh;
const envelopeRaw = read24('envelope_kwh', 'envelope_csv');
const envelope = envelopeRaw ? envelopeRaw.map(v => Math.max(0, num(v, 0))) : null;
@@ -137,6 +136,49 @@ const dayKey = (ms) =>
const currentHourStart = Math.floor(nowMs / 3600000) * 3600000;
const today = dayKey(nowMs);
// Une courbe est utilisable si elle DECRIT le jour dont on a besoin, pas si
// elle est recente. Une prevision publiee a 23:45 pour le 8 septembre reste
// exacte a 03:00 : la nuit, il n'y a rien a reactualiser. Inversement, une
// courbe publiee il y a dix minutes mais qui decrit la veille ne vaut rien.
//
// `forecast_day0` porte la date de la courbe « today » telle qu'elle etait au
// moment de la publication. Deux cas seulement sont exploitables :
// day0 = aujourd'hui -> lecture directe, cas normal
// day0 = hier -> la publication date d'avant minuit : la courbe
// « tomorrow » est celle d'aujourd'hui, et on n'a
// rien pour demain (repli sur l'enveloppe).
// Au-dela, les courbes sont trop vieilles pour dire quoi que ce soit.
//
// ⚠️ REPLI : `forecast_day0` absent ou illisible => on retombe sur l'ancienne
// garde d'age. Tant que le helper n'existe pas cote Home Assistant, le
// comportement est donc strictement inchange.
const forecastDay0 = (input.forecast_day0 || '').toString().trim();
const day0Lisible = /^\d{4}-\d{2}-\d{2}$/.test(forecastDay0);
const veille = dayKey(nowMs - 86400000);
let hourlyForToday = null;
let hourlyForTomorrow = null;
let forecastAlignment;
if (!day0Lisible) {
// Comportement historique, tant que la date n'est pas publiee.
forecastAlignment = 'date_absente';
if (forecastFresh) {
hourlyForToday = hourlyToday;
hourlyForTomorrow = hourlyTomorrow;
}
} else if (forecastDay0 === today) {
forecastAlignment = 'a_jour';
hourlyForToday = hourlyToday;
hourlyForTomorrow = hourlyTomorrow;
} else if (forecastDay0 === veille) {
forecastAlignment = 'decalee_d_un_jour';
hourlyForToday = hourlyTomorrow;
hourlyForTomorrow = null;
} else {
forecastAlignment = 'perimee';
}
const hourlyAvailable = !!hourlyForToday;
// Une heure ou l'enveloppe demontree est nulle est une heure de nuit : il n'y
// a jamais eu de soleil a y observer. Son taux de censure vaut 1 pour cette
// raison-la, pas parce que la mesure serait douteuse, et il n'existe aucune
@@ -174,9 +216,17 @@ const tomorrowSlots = slots.filter(s => s.day !== today);
if (hourlyAvailable) {
// Lecture directe : chaque heure porte déjà son énergie prévue.
for (const slot of todaySlots) slot.expectedKwh = Math.max(0, hourlyToday[slot.hour] ?? 0);
const tom = hourlyTomorrow || hourlyToday;
for (const slot of tomorrowSlots) slot.expectedKwh = Math.max(0, tom[slot.hour] ?? 0);
for (const slot of todaySlots) slot.expectedKwh = Math.max(0, hourlyForToday[slot.hour] ?? 0);
if (hourlyForTomorrow) {
for (const slot of tomorrowSlots) {
slot.expectedKwh = Math.max(0, hourlyForTomorrow[slot.hour] ?? 0);
}
} else {
// Courbe decalee : on connait aujourd'hui, pas demain. L'enveloppe mesuree
// reprend la main pour la suite de l'horizon plutot que de recopier la
// courbe du jour, qui serait une invention.
distribute(tomorrowSlots, tomorrowKwh);
}
} else {
distribute(todaySlots, todayRemainingKwh);
distribute(tomorrowSlots, tomorrowKwh);
@@ -221,6 +271,8 @@ return [{
hourlyCurveAvailable: !!hourlyToday,
forecastAgeMin: Number.isFinite(forecastAgeMin) ? Math.round(forecastAgeMin) : null,
forecastFresh,
forecastDay0: forecastDay0 || null,
forecastAlignment,
hoursAboveEnvelope: slots.filter(s => s.aboveEnvelope).length,
shapeConfidence: projected > 0 ? round2(1 - projectedOnWeakShape / projected) : null,
weakShapeHours: slots