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:
134
tools/gen-solar-forecast-workflow.mjs
Normal file
134
tools/gen-solar-forecast-workflow.mjs
Normal 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)`);
|
||||
@@ -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)
|
||||
|
||||
@@ -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' :
|
||||
|
||||
Reference in New Issue
Block a user