Chantier etape 5 : bilan energetique publie en mode ombre
Workflow n8n "EVCS Energy Balance V1" (id QoSfPeyGq35orF1K), actif, cycle de 5 min : snapshot HA -> CENSORSHIP et RESOURCE_ESTIMATE en parallele -> PREPARE_BALANCE -> BALANCE -> BUILD_HA_CALLS -> appels de service HA. MODE OMBRE garanti structurellement : BUILD_HA_CALLS n'ecrit que vers des entites evcs_balance_* plus une trace logbook. La suite de tests echoue si une ecriture sort de ce perimetre — verifie par mutation, l'injection d'un turn_on sur switch.edf_general_ok_switch fait rougir la suite. Kill-switch input_boolean.evcs_balance_enabled : a OFF, aucune ecriture du tout. Package HA evcs_energy_balance.yaml : 18 helpers. Profils de consommation, enveloppe de production et taux de censure amorces avec les valeurs calculees sur 28 jours reels ; capacite utile posee a 15 kWh (7 modules actifs). Outillage : gen-energy-balance-workflow.mjs, enregistrement dans lib-node-map.mjs, suite cablee dans n8n-push.mjs (bloquante). Suite a 24 scenarios. Premiere execution reelle : le bilan s'est declare indisponible faute d'entrees, exactement comme prevu, au lieu d'inventer des valeurs. Le format du snapshot (corps JSON dans `data`) et la lecture des profils CSV ont ete corriges ensuite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
@@ -41,7 +41,16 @@ function round2(v) {
|
||||
return Number.isFinite(v) ? Math.round(v * 100) / 100 : null;
|
||||
}
|
||||
|
||||
const input = $input.first().json;
|
||||
// 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 input = unwrapSnapshot($input.first().json);
|
||||
|
||||
const CFG = {
|
||||
TIMEZONE: 'Europe/Paris',
|
||||
@@ -55,14 +64,26 @@ const nowMs = num(input.nowMs, Date.now());
|
||||
const todayRemainingKwh = Math.max(0, num(input.forecast_today_remaining_kwh, 0));
|
||||
const tomorrowKwh = Math.max(0, num(input.forecast_tomorrow_kwh, 0));
|
||||
|
||||
// Enveloppe et taux de censure par heure : produits par le calcul quotidien.
|
||||
// Enveloppe et taux de censure par heure : produits par le calcul quotidien et
|
||||
// stockés dans des helpers input_text sous forme de CSV compact. On accepte
|
||||
// aussi le tableau déjà décodé, ce qui rend le nœud testable hors n8n.
|
||||
// Absents au premier démarrage → on le dit, on ne fabrique pas de forme.
|
||||
const envelope = Array.isArray(input.envelope_kwh) && input.envelope_kwh.length === 24
|
||||
? input.envelope_kwh.map(v => Math.max(0, num(v, 0)))
|
||||
: null;
|
||||
function read24(arrayKey, csvKey) {
|
||||
const direct = input[arrayKey];
|
||||
if (Array.isArray(direct) && direct.length === 24) return direct.map(v => num(v, NaN));
|
||||
const csv = input[csvKey];
|
||||
if (typeof csv !== 'string') return null;
|
||||
const parts = csv.split(',').map(v => Number.parseFloat(v.trim()));
|
||||
if (parts.length !== 24 || parts.some(v => !Number.isFinite(v))) return null;
|
||||
return parts;
|
||||
}
|
||||
|
||||
const censorshipRate = Array.isArray(input.censorship_rate) && input.censorship_rate.length === 24
|
||||
? input.censorship_rate.map(v => Math.max(0, Math.min(1, num(v, 1))))
|
||||
const envelopeRaw = read24('envelope_kwh', 'envelope_csv');
|
||||
const envelope = envelopeRaw ? envelopeRaw.map(v => Math.max(0, num(v, 0))) : null;
|
||||
|
||||
const censorshipRaw = read24('censorship_rate', 'censorship_rate_csv');
|
||||
const censorshipRate = censorshipRaw
|
||||
? censorshipRaw.map(v => Math.max(0, Math.min(1, num(v, 1))))
|
||||
: Array.from({ length: 24 }, () => 1);
|
||||
|
||||
const hourOfDay = (ms) =>
|
||||
|
||||
Reference in New Issue
Block a user