Chantier etapes 5b et 5c : recalcul quotidien des profils et fenetre de deficit suivante
5c — BALANCE ne repondait qu'a "combien jusqu'a ce que le solaire prenne le relais". A 15 h la reponse est zero : exacte mais inutile, le deficit qui approche etant celui de la nuit. Ajout de la prochaine traversee deficitaire (heure de debut, energie, SOC a atteindre avant). Ce SOC n'est PAS ecrete a 100 % : au-dela, meme une batterie pleine ne passe pas, et le manque est publie comme storage_shortfall_kwh — c'est le chiffre du dimensionnement. 5b — Workflow "EVCS Energy Profiles Daily V1" (N6UaB4ZozsbRPrFb), actif, 03:30 : lit /api/history/period sur 30 jours glissants et republie les quatre CSV (conso p50 et p80, enveloppe de production, taux de censure). CONSUMPTION_PROFILE devient DAILY_PROFILES et sait desormais decoder la reponse brute de l'API en plus des series deja pretes. Piege trouve : sans end_time, /api/history/period ne renvoie qu'UNE journee, silencieusement. Mes premieres mesures "sur 3 jours" n'en couvraient qu'une. Validation croisee : profils calcules depuis l'API REST (etats bruts, 11 j) contre profils calcules depuis les statistiques long terme (28 j) — 0,20 kWh/h d'ecart absolu moyen, par deux chemins de donnees independants. BUILD_HA_CALLS_PROFILES refuse d'ecraser les profils en place si l'historique ne couvre pas les 24 heures. Meme garantie de perimetre que le bilan. Suite a 27 scenarios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
115
tools/gen-energy-profiles-workflow.mjs
Normal file
115
tools/gen-energy-profiles-workflow.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
// Génère le squelette du workflow "EVCS Energy Profiles Daily V1" : recalcul
|
||||
// quotidien des profils horaires (consommation, enveloppe de production, taux
|
||||
// de censure) à partir de l'historique Home Assistant.
|
||||
// Le CODE des nodes est injecté depuis src/energy-balance/ par n8n-build.mjs.
|
||||
|
||||
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 ENTITIES = [
|
||||
'sensor.airtria_mpii_v1_energy_from_inverter_to_ac_out',
|
||||
'sensor.airtria_mpii_v1_energy_from_ac_in_1_to_ac_out',
|
||||
'sensor.evcs_hq2239dnt7p_total_energy',
|
||||
'sensor.gx_device_pv_energy',
|
||||
'sensor.victron_battery_soc',
|
||||
].join(',');
|
||||
|
||||
// Fenêtre demandée : 30 jours. L'API ne renvoie que ce que la rétention
|
||||
// conserve (10 jours au 24/08, 90 une fois la nouvelle rétention accumulée),
|
||||
// donc la fenêtre s'élargira d'elle-même sans rien changer ici.
|
||||
// ⚠️ `end_time` est OBLIGATOIRE : sans lui, /api/history/period ne renvoie
|
||||
// qu'UNE SEULE journée, silencieusement.
|
||||
const HISTORY_URL =
|
||||
"={{ '" + HA_URL + "/api/history/period/' " +
|
||||
'+ new Date(Date.now() - 30 * 86400000).toISOString() ' +
|
||||
"+ '?end_time=' + encodeURIComponent(new Date().toISOString()) " +
|
||||
"+ '&filter_entity_id=" + ENTITIES + "' " +
|
||||
"+ '&minimal_response&significant_changes_only=0' }}";
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
name: 'Schedule Daily',
|
||||
type: 'n8n-nodes-base.scheduleTrigger',
|
||||
typeVersion: 1.2,
|
||||
position: [0, 0],
|
||||
parameters: { rule: { interval: [{ field: 'days', triggerAtHour: 3, triggerAtMinute: 30 }] } },
|
||||
},
|
||||
{
|
||||
name: 'HA History',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [220, 0],
|
||||
parameters: {
|
||||
method: 'GET',
|
||||
url: HISTORY_URL,
|
||||
authentication: 'genericCredentialType',
|
||||
genericAuthType: 'httpBearerAuth',
|
||||
options: {
|
||||
timeout: 120000,
|
||||
response: { response: { responseFormat: 'text' } },
|
||||
},
|
||||
},
|
||||
credentials: { httpBearerAuth: HA_CRED },
|
||||
},
|
||||
{
|
||||
name: 'DAILY_PROFILES',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [440, 0],
|
||||
parameters: { jsCode: '// injecté depuis src/energy-balance/DAILY_PROFILES.js' },
|
||||
},
|
||||
{
|
||||
name: 'BUILD_HA_CALLS_PROFILES',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [660, 0],
|
||||
parameters: { jsCode: '// injecté depuis src/energy-balance/BUILD_HA_CALLS_PROFILES.js' },
|
||||
},
|
||||
{
|
||||
name: 'Execute HA Service Profiles',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [880, 0],
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
const connections = {
|
||||
'Schedule Daily': { main: [[{ node: 'HA History', type: 'main', index: 0 }]] },
|
||||
'HA History': { main: [[{ node: 'DAILY_PROFILES', type: 'main', index: 0 }]] },
|
||||
DAILY_PROFILES: { main: [[{ node: 'BUILD_HA_CALLS_PROFILES', type: 'main', index: 0 }]] },
|
||||
BUILD_HA_CALLS_PROFILES: { main: [[{ node: 'Execute HA Service Profiles', type: 'main', index: 0 }]] },
|
||||
};
|
||||
|
||||
const workflow = {
|
||||
id: 'N6UaB4ZozsbRPrFb',
|
||||
name: 'EVCS Energy Profiles Daily V1',
|
||||
nodes,
|
||||
connections,
|
||||
settings: { executionOrder: 'v1', callerPolicy: 'workflowsFromSameOwner' },
|
||||
staticData: null,
|
||||
};
|
||||
|
||||
fs.writeFileSync('workflows/evcs-energy-profiles-daily-v1.json', JSON.stringify(workflow, null, 2) + '\n');
|
||||
console.log(`Squelette écrit : workflows/evcs-energy-profiles-daily-v1.json (${nodes.length} nodes)`);
|
||||
@@ -41,6 +41,15 @@ export const WORKFLOWS = [
|
||||
{ file: 'HA_SNAPSHOT_TEMPLATE.json.tpl', node: 'HA Snapshot Balance', kind: 'template' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'energy-profiles',
|
||||
workflowFile: 'workflows/evcs-energy-profiles-daily-v1.json',
|
||||
srcDir: 'src/energy-balance',
|
||||
nodes: [
|
||||
{ file: 'DAILY_PROFILES.js', node: 'DAILY_PROFILES', kind: 'jsCode' },
|
||||
{ file: 'BUILD_HA_CALLS_PROFILES.js', node: 'BUILD_HA_CALLS_PROFILES', kind: 'jsCode' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Rétro-compatibilité (outils historiques mono-workflow)
|
||||
|
||||
@@ -109,7 +109,8 @@ 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 isBalanceWorkflow = path.basename(INPUT) === 'evcs-energy-balance-v1.json'
|
||||
|| path.basename(INPUT) === 'evcs-energy-profiles-daily-v1.json';
|
||||
const skipGolden = process.argv.includes('--skip-golden');
|
||||
const testSuite =
|
||||
isEvcsWorkflow && fs.existsSync('tests/golden/run.mjs') ? 'tests/golden/run.mjs' :
|
||||
|
||||
Reference in New Issue
Block a user