Chantier : recuperation de la courbe horaire Forecast.Solar

Workflow "EVCS Solar Forecast V1" (07P8I349ugna9FH4), actif, cadence 15 min —
la frequence de rafraichissement de la prevision elle-meme, l'API precisant
qu'interroger plus souvent n'a aucun interet. 96 appels par jour pour un quota
de 300 par heure.

Rien n'est code en dur : la cle d'API, les plans et les coordonnees viennent
tous de Home Assistant. La cle vit dans un input_text en mode password, exclue
du recorder, et n'apparait ni dans le depot ni dans le JSON du workflow —
l'URL est assemblee a l'execution par BUILD_FORECAST_URL, qui refuse
explicitement de construire une URL incomplete plutot que de gaspiller le
quota sur un appel malforme.

BUILD_HA_CALLS_FORECAST ne publie rien en cas d'echec plutot que d'ecraser une
prevision valide par du vide.

Suite portee a 33 scenarios, dont le refus sur cle absente, sur plans mal
formes, sur kill-switch, et la preservation de la prevision precedente en cas
d'echec.

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-25 02:30:28 +02:00
parent 1ce1237965
commit 26fb9f72ea
10 changed files with 636 additions and 2 deletions

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env node
// Génère le squelette du workflow "EVCS Solar Forecast V1" : récupération de la
// courbe horaire Forecast.Solar (abonnement Professional) et publication dans
// les helpers du bilan.
//
// Cadence : 15 min. C'est la fréquence de rafraîchissement de la prévision
// elle-même — l'API précise qu'interroger plus souvent n'a aucun intérêt.
// 96 appels par jour pour un quota de 300 par heure.
//
// La clé d'API n'apparaît NULLE PART ici : elle est lue dans un helper Home
// Assistant par le snapshot, et l'URL est assemblée par BUILD_FORECAST_URL.
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 Forecast',
type: 'n8n-nodes-base.scheduleTrigger',
typeVersion: 1.2,
position: [0, 0],
parameters: { rule: { interval: [{ field: 'minutes', minutesInterval: 15 }] } },
},
{
name: 'HA Snapshot Forecast',
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: 'BUILD_FORECAST_URL',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [440, 0],
parameters: { jsCode: '// injecté depuis src/energy-balance/BUILD_FORECAST_URL.js' },
},
{
name: 'Forecast.Solar',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [660, 0],
parameters: {
method: 'GET',
// URL assemblée par le nœud précédent : la clé ne transite pas par ce fichier.
url: '={{ $json.url }}',
options: {
timeout: 30000,
response: { response: { responseFormat: 'text', neverError: true } },
},
},
// Si l'URL est nulle (clé absente, kill-switch), l'appel échoue sans casser
// la chaîne : PARSE_FORECAST le détectera et rien ne sera publié.
onError: 'continueRegularOutput',
},
{
name: 'PARSE_FORECAST',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [880, 0],
parameters: { jsCode: '// injecté depuis src/energy-balance/PARSE_FORECAST.js' },
},
{
name: 'BUILD_HA_CALLS_FORECAST',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [1100, 0],
parameters: { jsCode: '// injecté depuis src/energy-balance/BUILD_HA_CALLS_FORECAST.js' },
},
{
name: 'Execute HA Service Forecast',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [1320, 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 Forecast': { main: [[{ node: 'HA Snapshot Forecast', type: 'main', index: 0 }]] },
'HA Snapshot Forecast': { main: [[{ node: 'BUILD_FORECAST_URL', type: 'main', index: 0 }]] },
BUILD_FORECAST_URL: { main: [[{ node: 'Forecast.Solar', type: 'main', index: 0 }]] },
'Forecast.Solar': { main: [[{ node: 'PARSE_FORECAST', type: 'main', index: 0 }]] },
PARSE_FORECAST: { main: [[{ node: 'BUILD_HA_CALLS_FORECAST', type: 'main', index: 0 }]] },
BUILD_HA_CALLS_FORECAST: { main: [[{ node: 'Execute HA Service Forecast', type: 'main', index: 0 }]] },
};
const workflow = {
id: '07P8I349ugna9FH4',
name: 'EVCS Solar Forecast V1',
nodes,
connections,
settings: { executionOrder: 'v1', callerPolicy: 'workflowsFromSameOwner' },
staticData: null,
};
fs.writeFileSync('workflows/evcs-solar-forecast-v1.json', JSON.stringify(workflow, null, 2) + '\n');
console.log(`Squelette écrit : workflows/evcs-solar-forecast-v1.json (${nodes.length} nodes)`);

View File

@@ -50,6 +50,17 @@ export const WORKFLOWS = [
{ file: 'BUILD_HA_CALLS_PROFILES.js', node: 'BUILD_HA_CALLS_PROFILES', kind: 'jsCode' },
],
},
{
id: 'solar-forecast',
workflowFile: 'workflows/evcs-solar-forecast-v1.json',
srcDir: 'src/energy-balance',
nodes: [
{ file: 'BUILD_FORECAST_URL.js', node: 'BUILD_FORECAST_URL', kind: 'jsCode' },
{ file: 'PARSE_FORECAST.js', node: 'PARSE_FORECAST', kind: 'jsCode' },
{ file: 'BUILD_HA_CALLS_FORECAST.js', node: 'BUILD_HA_CALLS_FORECAST', kind: 'jsCode' },
{ file: 'HA_SNAPSHOT_FORECAST.json.tpl', node: 'HA Snapshot Forecast', kind: 'template' },
],
},
];
// Rétro-compatibilité (outils historiques mono-workflow)

View File

@@ -110,7 +110,8 @@ const isEvcsWorkflow = path.basename(INPUT) === 'evcs-surplus-controller-v2.6.js
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'
|| path.basename(INPUT) === 'evcs-energy-profiles-daily-v1.json';
|| path.basename(INPUT) === 'evcs-energy-profiles-daily-v1.json'
|| path.basename(INPUT) === 'evcs-solar-forecast-v1.json';
const skipGolden = process.argv.includes('--skip-golden');
const testSuite =
isEvcsWorkflow && fs.existsSync('tests/golden/run.mjs') ? 'tests/golden/run.mjs' :