Files
EVCS/src/energy-balance/PREPARE_BALANCE.js
Tallagrand Thierry 6ae25ce339 Le nombre de modules se lit, il ne se code plus
La courbe energie/SOC a ete mesuree sur sept modules. Retirer un
module divise la capacite sans changer la forme de la courbe : les
pentes sont donc mises a l'echelle du nombre reellement en service,
lu dans input_number.evcs_balance_modules_en_service. Le controleur
EVCS lit le meme helper pour sa capacite utile - les deux decrivent
forcement le meme parc.

Helper absent, illisible ou hors bornes => sept, la valeur de la
mesure. Les 44 scenarios du bilan et les 80 golden passent donc sans
qu'un seul attendu ait ete touche : la bascule ne change rien tant
que personne ne l'ecrit.

Deux scenarios couvrent la bascule elle-meme, l'un avec quatre
modules (SOC exige 30,5 -> 35,2 %), l'autre avec une valeur
aberrante qui doit retomber sur sept. Preuve par mutation : ignorer
l'echelle fait tomber le premier et lui seul.

Le genou a 33 % est conserve par PRUDENCE et non par mesure :
l'hypothese est qu'il vient des cellules faibles du pack retire, donc
qu'il disparait avec lui. Le garder revient a sous-estimer la
reserve, c'est-a-dire a se tromper du bon cote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-09-05 18:54:57 +02:00

62 lines
2.6 KiB
JavaScript

// EVCS Energy Balance — PREPARE_BALANCE
// =====================================================================
// Colle de câblage n8n, et rien d'autre. Les nœuds de calcul (CENSORSHIP,
// RESOURCE_ESTIMATE, BALANCE) sont volontairement écrits pour ne lire que
// `$input`, afin de rester testables hors de n8n. Ce nœud rassemble donc ce
// que BALANCE attend, en puisant dans les nœuds précédents.
//
// Il fait aussi la seule conversion nécessaire : les profils horaires vivent
// dans des helpers `input_text` sous forme de CSV compact (24 valeurs, moins
// de 255 caractères), il faut les redonner sous forme de tableaux.
// =====================================================================
function parseCsv24(raw) {
if (typeof raw !== 'string') return null;
const parts = raw.split(',').map(v => Number.parseFloat(v.trim()));
if (parts.length !== 24 || parts.some(v => !Number.isFinite(v))) return null;
return parts;
}
// Le nœud HTTP de n8n renvoie le corps du template dans `data`, sous forme de
// chaîne. Hors n8n (tests), l'objet arrive déjà décodé — on accepte les deux.
function unwrapSnapshot(raw) {
if (raw && typeof raw.data === 'string') {
try { return JSON.parse(raw.data); } catch { return raw; }
}
return (raw && raw.data) || raw;
}
const snapshot = unwrapSnapshot($('HA Snapshot Balance').first().json);
const resource = $input.first().json;
return [{
json: {
nowMs: Number.parseFloat(snapshot.nowMs),
consumption_p50: parseCsv24(snapshot.consumption_p50_csv),
consumption_p80: parseCsv24(snapshot.consumption_p80_csv),
horizon: resource.horizon || [],
soc_percent: snapshot.soc_percent,
usable_capacity_kwh: snapshot.usable_capacity_kwh,
modules_en_service: snapshot.modules_en_service,
// Plancher dur : même valeur de repli que INPUT_PARSE.js
// (evcs_night_battery_hard_floor_pct n'est aujourd'hui exposé par AUCUN
// helper — la valeur « 22 % » documentée partout est en réalité un défaut
// de code, cf. backlog du chantier).
soc_floor_percent: 22,
// Vigilance Météo-France, lue sans connaître la commune (l'entité
// s'appelle sensor.<commune>_weather_alert). Absente tant que
// l'intégration n'est pas installée : le bilan publie alors
// 'indisponible' et ne change rien.
storm_alert_level: snapshot.storm_alert_level,
storm_alert_detail: snapshot.storm_alert_detail,
// Repris pour l'écriture des helpers, sans influencer le calcul.
resourceUsable: resource.usable,
shapeConfidence: resource.source ? resource.source.shapeConfidence : null,
},
}];