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:
Tallagrand Thierry
2026-08-24 15:43:37 +02:00
parent 9b24b5e29c
commit e36f050a42
11 changed files with 698 additions and 10 deletions

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env node
// Génère le squelette du workflow "EVCS Energy Balance V1" (structure, types,
// credentials, positions). Le CODE des nodes est ensuite injecté depuis
// src/energy-balance/ par tools/n8n-build.mjs — src/ reste la source de vérité.
// Rejouable : réécrit le squelette à l'identique.
import fs from 'node:fs';
const HA_CRED = { id: 'uv3sknYYigxBkaVN', name: 'Bearer Auth account 2' };
const HA_URL = 'http://10.10.10.17:8123';
const nodes = [
{
name: 'Schedule Trigger',
type: 'n8n-nodes-base.scheduleTrigger',
typeVersion: 1.2,
position: [0, 0],
parameters: { rule: { interval: [{ field: 'minutes', minutesInterval: 5 }] } },
},
{
name: 'HA Snapshot Balance',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [220, 0],
parameters: {
method: 'POST',
url: `${HA_URL}/api/template`,
authentication: 'genericCredentialType',
genericAuthType: 'httpBearerAuth',
sendHeaders: true,
headerParameters: {
parameters: [
{ name: 'Content-Type', value: 'application/json' },
{ name: 'Accept', value: 'text/plain' },
],
},
sendBody: true,
bodyParameters: { parameters: [{ name: 'template', value: '(injecté depuis src/energy-balance)' }] },
options: { timeout: 15000 },
},
credentials: { httpBearerAuth: HA_CRED },
},
{
name: 'CENSORSHIP',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [440, -120],
parameters: { jsCode: '// injecté depuis src/energy-balance/CENSORSHIP.js' },
},
{
name: 'RESOURCE_ESTIMATE',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [440, 120],
parameters: { jsCode: '// injecté depuis src/energy-balance/RESOURCE_ESTIMATE.js' },
},
{
name: 'PREPARE_BALANCE',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [660, 120],
parameters: { jsCode: '// injecté depuis src/energy-balance/PREPARE_BALANCE.js' },
},
{
name: 'BALANCE',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [880, 120],
parameters: { jsCode: '// injecté depuis src/energy-balance/BALANCE.js' },
},
{
name: 'BUILD_HA_CALLS',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [1100, 120],
parameters: { jsCode: '// injecté depuis src/energy-balance/BUILD_HA_CALLS.js' },
},
{
name: 'Execute HA Service Balance',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [1320, 120],
parameters: {
method: 'POST',
url: `={{ '${HA_URL}/api/services/' + $json.domain + '/' + $json.service }}`,
authentication: 'genericCredentialType',
genericAuthType: 'httpBearerAuth',
sendHeaders: true,
headerParameters: {
parameters: [
{ name: 'Content-Type', value: 'application/json' },
{ name: 'accept', value: 'application/json' },
],
},
sendBody: true,
specifyBody: 'json',
jsonBody: '={{ $json.data }}',
options: { timeout: 15000 },
},
credentials: { httpBearerAuth: HA_CRED },
onError: 'continueRegularOutput',
},
];
// Le snapshot alimente DEUX branches : CENSORSHIP qualifie l'instant présent,
// RESOURCE_ESTIMATE projette la production. PREPARE_BALANCE les réunit.
const connections = {
'Schedule Trigger': { main: [[{ node: 'HA Snapshot Balance', type: 'main', index: 0 }]] },
'HA Snapshot Balance': {
main: [[
{ node: 'CENSORSHIP', type: 'main', index: 0 },
{ node: 'RESOURCE_ESTIMATE', type: 'main', index: 0 },
]],
},
RESOURCE_ESTIMATE: { main: [[{ node: 'PREPARE_BALANCE', type: 'main', index: 0 }]] },
PREPARE_BALANCE: { main: [[{ node: 'BALANCE', type: 'main', index: 0 }]] },
BALANCE: { main: [[{ node: 'BUILD_HA_CALLS', type: 'main', index: 0 }]] },
BUILD_HA_CALLS: { main: [[{ node: 'Execute HA Service Balance', type: 'main', index: 0 }]] },
};
const workflow = {
id: 'QoSfPeyGq35orF1K',
name: 'EVCS Energy Balance V1',
nodes,
connections,
settings: { executionOrder: 'v1', callerPolicy: 'workflowsFromSameOwner' },
staticData: null,
};
fs.writeFileSync('workflows/evcs-energy-balance-v1.json', JSON.stringify(workflow, null, 2) + '\n');
console.log(`Squelette écrit : workflows/evcs-energy-balance-v1.json (${nodes.length} nodes)`);
console.log('→ node tools/n8n-build.mjs workflows/evcs-energy-balance-v1.json');

View File

@@ -28,6 +28,19 @@ export const WORKFLOWS = [
{ file: 'HA_SNAPSHOT_TEMPLATE.json.tpl', node: 'HA Snapshot Advisor', kind: 'template' },
],
},
{
id: 'energy-balance',
workflowFile: 'workflows/evcs-energy-balance-v1.json',
srcDir: 'src/energy-balance',
nodes: [
{ file: 'CENSORSHIP.js', node: 'CENSORSHIP', kind: 'jsCode' },
{ file: 'RESOURCE_ESTIMATE.js', node: 'RESOURCE_ESTIMATE', kind: 'jsCode' },
{ file: 'PREPARE_BALANCE.js', node: 'PREPARE_BALANCE', kind: 'jsCode' },
{ file: 'BALANCE.js', node: 'BALANCE', kind: 'jsCode' },
{ file: 'BUILD_HA_CALLS.js', node: 'BUILD_HA_CALLS', kind: 'jsCode' },
{ file: 'HA_SNAPSHOT_TEMPLATE.json.tpl', node: 'HA Snapshot Balance', kind: 'template' },
],
},
];
// Rétro-compatibilité (outils historiques mono-workflow)

View File

@@ -109,11 +109,13 @@ console.log('Syntaxe OK sur tous les nodes jsCode.');
const isEvcsWorkflow = path.basename(INPUT) === 'evcs-surplus-controller-v2.6.json';
const isAdvisorWorkflow = path.basename(INPUT) === 'evcs-advisor-shadow-v1.json';
const isHysteresisWorkflow = path.basename(INPUT) === 'victron-grid-hysteresis-v1.json';
const isBalanceWorkflow = path.basename(INPUT) === 'evcs-energy-balance-v1.json';
const skipGolden = process.argv.includes('--skip-golden');
const testSuite =
isEvcsWorkflow && fs.existsSync('tests/golden/run.mjs') ? 'tests/golden/run.mjs' :
isAdvisorWorkflow && fs.existsSync('tests/advisor/run.mjs') ? 'tests/advisor/run.mjs' :
isHysteresisWorkflow && fs.existsSync('tests/golden-hysteresis/run.mjs') ? 'tests/golden-hysteresis/run.mjs' :
isBalanceWorkflow && fs.existsSync('tests/energy-balance/run.mjs') ? 'tests/energy-balance/run.mjs' :
null;
if (testSuite) {
if (skipGolden) {