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
53 lines
2.3 KiB
JavaScript
53 lines
2.3 KiB
JavaScript
// EVCS Energy Balance — BUILD_FORECAST_URL
|
||
// =====================================================================
|
||
// Construit l'URL d'appel à Forecast.Solar. Rien n'est codé en dur : la clé,
|
||
// les plans et les coordonnées viennent tous de Home Assistant.
|
||
//
|
||
// ⚠️ CONVENTION D'AZIMUT : l'API utilise 0 = sud, −90 = est, +90 = ouest,
|
||
// alors que Home Assistant utilise 0 = nord. Le helper
|
||
// `evcs_balance_forecast_planes` contient DÉJÀ les valeurs converties
|
||
// (azimut_API = azimut_HA − 180), au format attendu : des triplets
|
||
// dec/azimut/kWc successifs séparés par des barres obliques.
|
||
// Les listes séparées par des virgules sont refusées par l'API (erreur 422).
|
||
//
|
||
// ⚠️ La clé apparaît dans l'URL, donc dans les données d'exécution n8n si
|
||
// l'enregistrement des erreurs est actif. Compromis assumé : l'API n'accepte
|
||
// la clé que dans le chemin, et l'instance n8n est privée.
|
||
// =====================================================================
|
||
|
||
function unwrapSnapshot(raw) {
|
||
if (raw && typeof raw.data === 'string') {
|
||
try { return JSON.parse(raw.data); } catch { return raw; }
|
||
}
|
||
return (raw && raw.data) || raw;
|
||
}
|
||
|
||
const input = unwrapSnapshot($input.first().json);
|
||
|
||
const enabled = (input.enabled ?? 'on').toString().toLowerCase() === 'on';
|
||
const key = (input.api_key || '').toString().trim();
|
||
const planes = (input.planes || '').toString().trim();
|
||
const lat = Number.parseFloat(input.latitude);
|
||
const lon = Number.parseFloat(input.longitude);
|
||
|
||
// Refus explicite plutôt qu'un appel malformé : une URL incomplète
|
||
// consommerait le quota pour rien et renverrait une erreur opaque.
|
||
const problems = [];
|
||
if (!enabled) problems.push('bilan désactivé (kill-switch)');
|
||
if (!key || ['unknown', 'unavailable'].includes(key.toLowerCase())) problems.push('clé API absente');
|
||
if (!/^[\d.\-\/]+$/.test(planes) || planes.split('/').length % 3 !== 0) problems.push('plans invalides');
|
||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) problems.push('coordonnées absentes');
|
||
|
||
if (problems.length) {
|
||
return [{ json: { ok: false, problems, url: null } }];
|
||
}
|
||
|
||
return [{
|
||
json: {
|
||
ok: true,
|
||
nowMs: Number.parseFloat(input.nowMs),
|
||
planeCount: planes.split('/').length / 3,
|
||
url: `https://api.forecast.solar/${key}/estimate/watthours/period/${lat}/${lon}/${planes}`,
|
||
},
|
||
}];
|