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

@@ -12,7 +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, profilesShadowScenarios } from './scenarios-shadow.mjs';
import { shadowScenarios, profilesShadowScenarios, forecastScenarios } from './scenarios-shadow.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
@@ -95,6 +95,17 @@ SUMMARIZE.BUILD_HA_CALLS = (calls) => {
};
};
SUMMARIZE.BUILD_HA_CALLS_PROFILES = SUMMARIZE.BUILD_HA_CALLS;
SUMMARIZE.BUILD_HA_CALLS_FORECAST = (calls) => {
const base = SUMMARIZE.BUILD_HA_CALLS(calls);
const entities = calls.filter(c => c.json.domain !== 'logbook').map(c => c.json.data.entity_id || '');
return { ...base, wroteForecast: entities.includes('input_text.evcs_balance_forecast_today') };
};
SUMMARIZE.BUILD_FORECAST_URL = (o) => ({
urlOk: o.ok,
problems: (o.problems || []).join(','),
planeCount: o.planeCount ?? null,
urlTail: o.url ? o.url.split('/period/')[1].split('/').slice(2).join('/') : null,
});
const all = [
...censorshipScenarios.map(s => ({ node: 'CENSORSHIP', ...s })),
@@ -103,6 +114,7 @@ const all = [
...balanceScenarios,
...shadowScenarios,
...profilesShadowScenarios,
...forecastScenarios,
];
let failed = 0;