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:
@@ -12,6 +12,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { scenarios as censorshipScenarios } from './scenarios.mjs';
|
||||
import { profileScenarios, resourceScenarios } from './scenarios-profile.mjs';
|
||||
import { balanceScenarios } from './scenarios-balance.mjs';
|
||||
import { shadowScenarios } from './scenarios-shadow.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
|
||||
@@ -23,9 +24,19 @@ function runNode(nodeName, snapshot) {
|
||||
if (!codeCache.has(nodeName)) {
|
||||
codeCache.set(nodeName, fs.readFileSync(path.join(ROOT, `src/energy-balance/${nodeName}.js`), 'utf8'));
|
||||
}
|
||||
const sandbox = { $input: { first: () => ({ json: snapshot }) }, console };
|
||||
// Certains nœuds sont de la colle n8n et lisent d'autres nœuds via $('Nom').
|
||||
const upstream = snapshot.__nodes || {};
|
||||
const sandbox = {
|
||||
$input: { first: () => ({ json: snapshot }) },
|
||||
$: (name) => {
|
||||
if (!(name in upstream)) throw new Error(`nœud amont non fourni au test : ${name}`);
|
||||
return { first: () => ({ json: upstream[name] }) };
|
||||
},
|
||||
console,
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
return vm.runInContext(`(function(){ ${codeCache.get(nodeName)} })()`, sandbox, { timeout: 15000 })[0].json;
|
||||
const out = vm.runInContext(`(function(){ ${codeCache.get(nodeName)} })()`, sandbox, { timeout: 15000 });
|
||||
return nodeName === 'BUILD_HA_CALLS' ? out : out[0].json;
|
||||
}
|
||||
|
||||
const SUMMARIZE = {
|
||||
@@ -64,11 +75,26 @@ const SUMMARIZE = {
|
||||
}) : ({ usable: false, missing: o.missing.join(',') }),
|
||||
};
|
||||
|
||||
// BUILD_HA_CALLS renvoie une LISTE d'appels, pas un objet unique.
|
||||
SUMMARIZE.BUILD_HA_CALLS = (calls) => {
|
||||
const entities = calls
|
||||
.filter(c => c.json.domain !== 'logbook')
|
||||
.map(c => c.json.data.entity_id || '');
|
||||
return {
|
||||
callCount: calls.length,
|
||||
outOfScope: entities.filter(e => !/^(input_text|input_number|input_boolean)\.evcs_balance_/.test(e)).join(','),
|
||||
wroteRequiredSoc: entities.includes('input_number.evcs_balance_required_soc'),
|
||||
wroteGridNeeded: entities.includes('input_boolean.evcs_balance_grid_needed'),
|
||||
hasLogbook: calls.some(c => c.json.domain === 'logbook'),
|
||||
};
|
||||
};
|
||||
|
||||
const all = [
|
||||
...censorshipScenarios.map(s => ({ node: 'CENSORSHIP', ...s })),
|
||||
...profileScenarios,
|
||||
...resourceScenarios,
|
||||
...balanceScenarios,
|
||||
...shadowScenarios,
|
||||
];
|
||||
|
||||
let failed = 0;
|
||||
|
||||
Reference in New Issue
Block a user