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

@@ -5593,3 +5593,95 @@ n'est jamais parti dans le depot.
--- ---
*Document mis a jour le : 08/09/2026 — acces API Grafana retabli, rectification sur la cause des 401* *Document mis a jour le : 08/09/2026 — acces API Grafana retabli, rectification sur la cause des 401*
## 61. Session du 08/09/2026 — La prevision se juge sur la date qu'elle decrit
### Le symptome
Nuit du 07 au 08/09, **32 calculs du bilan sur 120 en statut « degrade »**,
tous entre 00 h et 03 h, avec ce motif :
> « Aucun croisement PV/conso sur l'horizon : 47,6 kWh a couvrir → SOC requis
> 100 %. ⚠ chiffre indicatif, pas une exigence datee. »
Le controleur exige `statut = ok` pour suivre le bilan (§39) : a ces moments-la
il retombait sur le palier de 35 %, d'ou une cible du matin qui oscillait entre
35, 78,8 et 85 % au lieu de decroitre proprement.
### La panne, et ce qu'elle a revele
`input_text.evcs_balance_forecast_updated` s'arrete a **23:45** et ne reprend
qu'a **04:30** — 4 h 45. Pendant ce temps le workflow s'executait bien quatre
fois par heure **en « success »**, parce que le noeud d'appel porte
`neverError: true` et `onError: continueRegularOutput`.
Reponse reelle de l'API pendant la fenetre : `socket hang up`. Et ce n'etait pas
que Forecast.Solar — le noeud `Execute HA Service Forecast` rendait **0 item**
la nuit contre 1 en journee : **Home Assistant lui aussi etait injoignable**
depuis n8n, si bien que le message d'echec ne pouvait meme pas etre publie.
Deux autres workflows ont eu des erreurs dans la meme fenetre. Cause racine de
l'indisponibilite reseau **non etablie** ; ce n'est pas la sauvegarde de 399 Go,
qui s'est terminee le 07 a 12:04. Une seule occurrence en quatre jours.
⚠️ **Ce que l'on controle, c'est la REACTION a cette panne, et elle etait
fautive.** Les courbes sont publiees dans `evcs_balance_forecast_today` et
`_tomorrow` **sans leur date**. `PARSE_FORECAST` prend `days[0]` comme
« aujourd'hui » : vrai a l'instant de la publication, **faux des qu'on passe
minuit sans rafraichir**. A 23:45 « today » etait la courbe du 7 ; a 00:45 le
bilan l'a lue comme celle du 8, journee dont toutes les heures etaient passees
— d'ou « aucune production sur l'horizon ». En temps normal le rafraichissement
de minuit corrige en quinze minutes et personne ne voit rien.
### Le correctif
**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 reste exacte a
03:00 : la nuit, il n'y a rien a reactualiser.
- `BUILD_HA_CALLS_FORECAST` publie `input_text.evcs_balance_forecast_day0`
(la date de la courbe « today ») ;
- le gabarit du bilan la lit ;
- `RESOURCE_ESTIMATE` remplace la garde d'age de 60 min par une garde de date,
avec trois cas : `a_jour` (lecture directe), `decalee_d_un_jour`
(publication d'avant minuit : la courbe « tomorrow » EST celle du jour, et
l'enveloppe mesuree reprend la main pour le lendemain), `perimee`
(au-dela : repli enveloppe).
⚠️ **REPLI** : `forecast_day0` absent ou illisible → ancienne garde d'age.
Tant que le helper n'existe pas cote Home Assistant, le comportement est
strictement inchange. C'est ce qui permet de deployer le code avant le helper.
### Visibilite de la panne
Le rapport de nuit signale desormais un trou de plus de deux heures dans les
publications de prevision. Il lit la table `prevision` sur **l'autre NAS** :
c'est le seul temoin qui survit quand le .17 est injoignable. Verifie sur la
nuit ecoulee : la requete rend bien 285 minutes.
### Tests 44 → 48
Les quatre scenarios partagent la meme heure — **00:45, l'instant exact du
decrochage** — et les memes courbes : seule la date publiee change, ce qui les
rend probants.
`prevision-decalee-apres-minuit` (le cas reel : day0 = la veille → la courbe du
lendemain devient celle du jour, 30 kWh redeviennent visibles) ·
`prevision-a-jour-lecture-directe` (contrefactuel) ·
`prevision-vieille-mais-du-bon-jour` (publiee cinq heures plus tot mais datee du
jour : utilisable — **l'ancienne garde d'age la refusait**) ·
`prevision-perimee-de-deux-jours` (repli enveloppe).
Un attendu existant passe de 4 a 5 ecritures (`forecast-perimetre-respecte`) :
la date est publiee a cote des courbes, et le perimetre `evcs_balance_*` reste
respecte.
Preuve par mutation, quatre directions, **chacune tombant sur un jeu different**
et chaque scenario ayant la sienne : decalage non corrige · cas decale non
reconnu · retour a la garde d'age (3 scenarios) · courbe perimee utilisee quand
meme.
### A FAIRE AVANT QUE LE CORRECTIF AGISSE
1. Creer le helper `input_text.evcs_balance_forecast_day0` dans
`/mnt/ARCH3/docker/HA/packages/evcs_energy_balance.yaml`, a cote de
`evcs_balance_forecast_today`, puis `input_text.reload` — aucun redemarrage.
2. Pousser les deux workflows : `evcs-solar-forecast-v1.json` (publie la date)
et `evcs-energy-balance-v1.json` (la lit).
L'ordre n'a pas d'importance grace au repli.
---
*Document mis a jour le : 08/09/2026 — prevision jugee sur sa date et non son age, tests 48/48, en attente de deploiement*

View File

@@ -31,6 +31,15 @@ if (!parsed.ok) {
return calls; 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); setText('input_text.evcs_balance_forecast_today', parsed.today.csv);
if (parsed.tomorrow) { if (parsed.tomorrow) {
setText('input_text.evcs_balance_forecast_tomorrow', parsed.tomorrow.csv); setText('input_text.evcs_balance_forecast_tomorrow', parsed.tomorrow.csv);

View File

@@ -11,6 +11,7 @@
"envelope_csv": states("input_text.evcs_balance_envelope"), "envelope_csv": states("input_text.evcs_balance_envelope"),
"censorship_rate_csv": states("input_text.evcs_balance_censorship_rate"), "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_today_csv": states("input_text.evcs_balance_forecast_today"),
"forecast_tomorrow_csv": states("input_text.evcs_balance_forecast_tomorrow"), "forecast_tomorrow_csv": states("input_text.evcs_balance_forecast_tomorrow"),
"forecast_updated": states("input_text.evcs_balance_forecast_updated"), "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 hourlyToday = read24('forecast_today_kwh', 'forecast_today_csv');
const hourlyTomorrow = read24('forecast_tomorrow_kwh', 'forecast_tomorrow_csv'); const hourlyTomorrow = read24('forecast_tomorrow_kwh', 'forecast_tomorrow_csv');
const hourlyAvailable = !!hourlyToday && forecastFresh;
const envelopeRaw = read24('envelope_kwh', 'envelope_csv'); const envelopeRaw = read24('envelope_kwh', 'envelope_csv');
const envelope = envelopeRaw ? envelopeRaw.map(v => Math.max(0, num(v, 0))) : null; 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 currentHourStart = Math.floor(nowMs / 3600000) * 3600000;
const today = dayKey(nowMs); 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 // 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 // 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 // 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) { if (hourlyAvailable) {
// Lecture directe : chaque heure porte déjà son énergie prévue. // 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); for (const slot of todaySlots) slot.expectedKwh = Math.max(0, hourlyForToday[slot.hour] ?? 0);
const tom = hourlyTomorrow || hourlyToday; if (hourlyForTomorrow) {
for (const slot of tomorrowSlots) slot.expectedKwh = Math.max(0, tom[slot.hour] ?? 0); 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 { } else {
distribute(todaySlots, todayRemainingKwh); distribute(todaySlots, todayRemainingKwh);
distribute(tomorrowSlots, tomorrowKwh); distribute(tomorrowSlots, tomorrowKwh);
@@ -221,6 +271,8 @@ return [{
hourlyCurveAvailable: !!hourlyToday, hourlyCurveAvailable: !!hourlyToday,
forecastAgeMin: Number.isFinite(forecastAgeMin) ? Math.round(forecastAgeMin) : null, forecastAgeMin: Number.isFinite(forecastAgeMin) ? Math.round(forecastAgeMin) : null,
forecastFresh, forecastFresh,
forecastDay0: forecastDay0 || null,
forecastAlignment,
hoursAboveEnvelope: slots.filter(s => s.aboveEnvelope).length, hoursAboveEnvelope: slots.filter(s => s.aboveEnvelope).length,
shapeConfidence: projected > 0 ? round2(1 - projectedOnWeakShape / projected) : null, shapeConfidence: projected > 0 ? round2(1 - projectedOnWeakShape / projected) : null,
weakShapeHours: slots weakShapeHours: slots

View File

@@ -65,6 +65,8 @@ const SUMMARIZE = {
RESOURCE_ESTIMATE: o => ({ RESOURCE_ESTIMATE: o => ({
usable: o.usable, usable: o.usable,
mode: o.mode, mode: o.mode,
// Quelle courbe a ete retenue, et pour quel jour (correctif du 08/09).
alignment: o.source.forecastAlignment,
todayExpected: o.todayExpectedRemainingKwh, todayExpected: o.todayExpectedRemainingKwh,
tomorrowExpected: o.tomorrowExpectedKwh, tomorrowExpected: o.tomorrowExpectedKwh,
shapeConfidenceBelow: o.source.shapeConfidence != null && o.source.shapeConfidence < 0.5, shapeConfidenceBelow: o.source.shapeConfidence != null && o.source.shapeConfidence < 0.5,

View File

@@ -137,8 +137,106 @@ export const profileScenarios = [
// --- RESOURCE_ESTIMATE ------------------------------------------------- // --- RESOURCE_ESTIMATE -------------------------------------------------
const envelope = JSON.parse(fs.readFileSync(path.join(HERE, 'fixture-envelope.json'), 'utf8')); const envelope = JSON.parse(fs.readFileSync(path.join(HERE, 'fixture-envelope.json'), 'utf8'));
const MIDDAY = Date.parse('2026-08-24T12:30:00+02:00'); const MIDDAY = Date.parse('2026-08-24T12:30:00+02:00');
// 00:45, l'heure exacte ou le bilan a decroche la nuit du 07 au 08/09.
const APRES_MINUIT = Date.parse('2026-09-08T00:45:00+02:00');
// Deux courbes horaires de 24 valeurs : une journee sans rien, et une journee
// a 30 kWh repartis de 8 h a 17 h (3 kWh par heure).
const JOURNEE_VIDE = Array.from({ length: 24 }, () => 0);
const JOURNEE_PLEINE = Array.from({ length: 24 }, (_, h) => (h >= 8 && h <= 17 ? 3 : 0));
export const resourceScenarios = [ export const resourceScenarios = [
// --- Correctif du 08/09/2026 : la courbe se juge sur la DATE qu'elle decrit,
// plus sur son age. Les quatre scenarios ci-dessous partagent la meme heure
// (00:45, juste apres minuit) et les memes courbes : seule la date publiee
// change. C'est ce qui les rend probants.
//
// Releve reel : la nuit du 07 au 08/09, plus aucune publication de 23:45 a
// 04:30. La derniere courbe decrivait le 7, dont toutes les heures etaient
// passees — le bilan a conclu « aucune production sur l'horizon ».
{
node: 'RESOURCE_ESTIMATE',
id: 'prevision-decalee-apres-minuit',
label: 'Publication d\'avant minuit : la courbe « lendemain » EST celle du jour, la production redevient visible',
snapshot: {
nowMs: APRES_MINUIT,
forecast_day0: '2026-09-07',
forecast_today_kwh: JOURNEE_VIDE,
forecast_tomorrow_kwh: JOURNEE_PLEINE,
forecast_today_remaining_kwh: '0',
forecast_tomorrow_kwh_total: '0',
envelope_kwh: envelope.envelope_kwh,
censorship_rate: envelope.censorship_rate,
},
expect: {
usable: true,
mode: 'courbe_horaire_api',
alignment: 'decalee_d_un_jour',
todayExpected: 30,
},
},
{
node: 'RESOURCE_ESTIMATE',
id: 'prevision-a-jour-lecture-directe',
label: 'CONTREFACTUEL : meme instant, courbe datee du jour → lecture directe',
snapshot: {
nowMs: APRES_MINUIT,
forecast_day0: '2026-09-08',
forecast_today_kwh: JOURNEE_PLEINE,
forecast_tomorrow_kwh: JOURNEE_VIDE,
forecast_today_remaining_kwh: '0',
envelope_kwh: envelope.envelope_kwh,
censorship_rate: envelope.censorship_rate,
},
expect: {
usable: true,
mode: 'courbe_horaire_api',
alignment: 'a_jour',
todayExpected: 30,
},
},
{
node: 'RESOURCE_ESTIMATE',
id: 'prevision-vieille-mais-du-bon-jour',
label: 'Publiee il y a cinq heures mais datee du jour : utilisable — l\'ancienne garde d\'age la refusait',
snapshot: {
nowMs: APRES_MINUIT,
forecast_day0: '2026-09-08',
forecast_updated: '07/09 19:45',
forecast_today_kwh: JOURNEE_PLEINE,
forecast_tomorrow_kwh: JOURNEE_VIDE,
forecast_today_remaining_kwh: '0',
envelope_kwh: envelope.envelope_kwh,
censorship_rate: envelope.censorship_rate,
},
expect: {
usable: true,
mode: 'courbe_horaire_api',
alignment: 'a_jour',
todayExpected: 30,
},
},
{
node: 'RESOURCE_ESTIMATE',
id: 'prevision-perimee-de-deux-jours',
label: 'CONTREFACTUEL : courbe datee de l\'avant-veille → inexploitable, repli sur l\'enveloppe',
snapshot: {
nowMs: APRES_MINUIT,
forecast_day0: '2026-09-06',
forecast_today_kwh: JOURNEE_PLEINE,
forecast_tomorrow_kwh: JOURNEE_PLEINE,
forecast_today_remaining_kwh: '0',
forecast_tomorrow_kwh_total: '0',
envelope_kwh: envelope.envelope_kwh,
censorship_rate: envelope.censorship_rate,
},
expect: {
usable: true,
mode: 'repli_enveloppe',
alignment: 'perimee',
todayExpected: 0,
},
},
{ {
node: 'RESOURCE_ESTIMATE', node: 'RESOURCE_ESTIMATE',
id: 'ressource-repartition-prevision', id: 'ressource-repartition-prevision',

View File

@@ -191,7 +191,9 @@ export const forecastScenarios = [
id: 'forecast-perimetre-respecte', id: 'forecast-perimetre-respecte',
label: 'Publication de la prévision : périmètre evcs_balance_* respecté', label: 'Publication de la prévision : périmètre evcs_balance_* respecté',
snapshot: FS_OK, snapshot: FS_OK,
expect: { outOfScope: '', callCount: 4, hasLogbook: true }, // 5 et non 4 depuis le 08/09 : la date que decrit la courbe est publiee a
// cote des courbes elles-memes (`forecast_day0`).
expect: { outOfScope: '', callCount: 5, hasLogbook: true },
}, },
{ {
node: 'BUILD_HA_CALLS_FORECAST', node: 'BUILD_HA_CALLS_FORECAST',

View File

@@ -167,6 +167,28 @@ export async function texteRapport(client, jour = new Date()) {
: 'C\'est du bruit de regulation, sans consequence.'); : 'C\'est du bruit de regulation, sans consequence.');
} }
// ------------------------------------------------ prevision solaire
// Une panne de prevision est invisible autrement : les nodes n8n portent
// `onError: continueRegularOutput`, donc les executions restent « success »
// meme quand l'appel echoue, et quand Home Assistant est lui aussi
// injoignable le message d'echec ne peut meme pas etre publie. Le service
// d'historisation, lui, tourne sur l'autre NAS : c'est le seul temoin qui
// survit. Releve de la nuit du 07 au 08/09 : 4 h 45 sans une seule
// publication, et personne ne l'a su avant le lendemain.
const { rows: [prev] } = await q(
`SELECT round(EXTRACT(EPOCH FROM max(ecart)) / 60) AS trou
FROM (SELECT publie_a - lag(publie_a) OVER (ORDER BY publie_a) AS ecart
FROM prevision WHERE publie_a >= $1 AND publie_a < $2) t`,
[debut, fin]);
const trouMin = Number(prev?.trou ?? 0);
if (trouMin >= 120) {
const h = Math.floor(trouMin / 60);
const m = trouMin % 60;
dire(`Attention, la prevision solaire n'a pas ete rafraichie pendant ${h} heure${h > 1 ? 's' : ''}`
+ `${m ? ` et ${m} minutes` : ''}. Le bilan a donc travaille sur une courbe`
+ ' vieillissante pendant ce temps, et la cible de la nuit a pu retomber sur la regle des paliers.');
}
// ------------------------------------------------ anomalies // ------------------------------------------------ anomalies
const { rows: soucis } = await q( const { rows: soucis } = await q(
`SELECT resume FROM evenement WHERE ts >= $1 AND ts < $2 `SELECT resume FROM evenement WHERE ts >= $1 AND ts < $2

File diff suppressed because one or more lines are too long

View File

@@ -122,7 +122,7 @@
0 0
], ],
"parameters": { "parameters": {
"jsCode": "// EVCS Energy Balance — BUILD_HA_CALLS_FORECAST\n// =====================================================================\n// Publie la prévision horaire dans ses helpers.\n// MÊME GARANTIE DE PÉRIMÈTRE : uniquement des entités `evcs_balance_*`.\n// En cas d'échec, on ne publie rien plutôt que d'écraser une prévision valide\n// par du vide — une prévision d'il y a quinze minutes vaut mieux que rien.\n// =====================================================================\n\nconst parsed = $input.first().json;\n\nfunction stampParis() {\n const p = new Intl.DateTimeFormat('fr-FR', {\n timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit',\n hour: '2-digit', minute: '2-digit', hour12: false,\n }).formatToParts(new Date());\n const g = t => p.find(x => x.type === t)?.value || '';\n return `${g('day')}/${g('month')} ${g('hour')}:${g('minute')}`;\n}\n\nconst calls = [];\nfunction setText(entity, value) {\n calls.push({ json: {\n domain: 'input_text', service: 'set_value',\n data: { entity_id: entity, value: (value ?? '').toString().slice(0, 250) },\n } });\n}\n\nif (!parsed.ok) {\n setText('input_text.evcs_balance_forecast_updated',\n `échec : ${(parsed.problems || []).join(', ')}`);\n return calls;\n}\n\nsetText('input_text.evcs_balance_forecast_today', parsed.today.csv);\nif (parsed.tomorrow) {\n setText('input_text.evcs_balance_forecast_tomorrow', parsed.tomorrow.csv);\n}\nsetText('input_text.evcs_balance_forecast_updated', stampParis());\n\ncalls.push({ json: {\n domain: 'logbook', service: 'log',\n data: {\n name: 'EVCS Bilan — prévision solaire',\n message: `${parsed.today.date} : ${parsed.today.totalKwh} kWh attendus`\n + (parsed.tomorrow ? `, lendemain ${parsed.tomorrow.totalKwh} kWh` : '')\n + ` (${parsed.days} jours reçus, quota restant ${parsed.rateLimitRemaining ?? '?'})`,\n entity_id: 'input_text.evcs_balance_forecast_today',\n },\n} });\n\nreturn calls;\n" "jsCode": "// EVCS Energy Balance — BUILD_HA_CALLS_FORECAST\n// =====================================================================\n// Publie la prévision horaire dans ses helpers.\n// MÊME GARANTIE DE PÉRIMÈTRE : uniquement des entités `evcs_balance_*`.\n// En cas d'échec, on ne publie rien plutôt que d'écraser une prévision valide\n// par du vide — une prévision d'il y a quinze minutes vaut mieux que rien.\n// =====================================================================\n\nconst parsed = $input.first().json;\n\nfunction stampParis() {\n const p = new Intl.DateTimeFormat('fr-FR', {\n timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit',\n hour: '2-digit', minute: '2-digit', hour12: false,\n }).formatToParts(new Date());\n const g = t => p.find(x => x.type === t)?.value || '';\n return `${g('day')}/${g('month')} ${g('hour')}:${g('minute')}`;\n}\n\nconst calls = [];\nfunction setText(entity, value) {\n calls.push({ json: {\n domain: 'input_text', service: 'set_value',\n data: { entity_id: entity, value: (value ?? '').toString().slice(0, 250) },\n } });\n}\n\nif (!parsed.ok) {\n setText('input_text.evcs_balance_forecast_updated',\n `échec : ${(parsed.problems || []).join(', ')}`);\n return calls;\n}\n\n// La DATE que decrit la courbe, et non l'heure a laquelle on l'a recue.\n// Sans elle, « today » veut dire « le premier jour renvoye par l'API au moment\n// de la publication » — ce qui devient faux des qu'on passe minuit sans\n// rafraichir. Nuit du 07 au 08/09 : plus aucune publication de 23:45 a 04:30,\n// et le bilan a lu comme « aujourd'hui » la courbe du 7, dont toutes les heures\n// etaient passees. Il en a conclu « aucun croisement, 47,6 kWh a couvrir, SOC\n// requis 100 % », s'est declare degrade, et le controleur est retombe sur le\n// palier de 35 % pendant quatre heures.\nsetText('input_text.evcs_balance_forecast_day0', parsed.today.date);\nsetText('input_text.evcs_balance_forecast_today', parsed.today.csv);\nif (parsed.tomorrow) {\n setText('input_text.evcs_balance_forecast_tomorrow', parsed.tomorrow.csv);\n}\nsetText('input_text.evcs_balance_forecast_updated', stampParis());\n\ncalls.push({ json: {\n domain: 'logbook', service: 'log',\n data: {\n name: 'EVCS Bilan — prévision solaire',\n message: `${parsed.today.date} : ${parsed.today.totalKwh} kWh attendus`\n + (parsed.tomorrow ? `, lendemain ${parsed.tomorrow.totalKwh} kWh` : '')\n + ` (${parsed.days} jours reçus, quota restant ${parsed.rateLimitRemaining ?? '?'})`,\n entity_id: 'input_text.evcs_balance_forecast_today',\n },\n} });\n\nreturn calls;\n"
} }
}, },
{ {