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:
@@ -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;
|
||||
|
||||
@@ -108,3 +108,60 @@ export const profilesShadowScenarios = [
|
||||
expect: { outOfScope: '', wroteProfiles: false },
|
||||
},
|
||||
];
|
||||
|
||||
// --- Prévision solaire (workflow 15 min) ---------------------------------
|
||||
const FS_OK = {
|
||||
ok: true, days: 6,
|
||||
today: { date: '2026-08-25', csv: '0.00,'.repeat(23) + '1.00', totalKwh: 43.5 },
|
||||
tomorrow: { date: '2026-08-26', csv: '0.00,'.repeat(23) + '2.00', totalKwh: 30.0 },
|
||||
rateLimitRemaining: 293,
|
||||
};
|
||||
|
||||
export const forecastScenarios = [
|
||||
{
|
||||
node: 'BUILD_FORECAST_URL',
|
||||
id: 'url-refus-cle-absente',
|
||||
label: 'Clé absente : refus explicite, aucun appel gaspillé sur le quota',
|
||||
snapshot: { enabled: 'on', api_key: 'unknown', planes: '40/-45/4.03', latitude: 48.7, longitude: -3.5 },
|
||||
expect: { urlOk: false, problems: 'clé API absente' },
|
||||
},
|
||||
{
|
||||
node: 'BUILD_FORECAST_URL',
|
||||
id: 'url-refus-plans-invalides',
|
||||
label: 'Plans mal formés (nombre de valeurs non multiple de 3) : refus',
|
||||
snapshot: { enabled: 'on', api_key: 'ABCD1234', planes: '40/-45', latitude: 48.7, longitude: -3.5 },
|
||||
expect: { urlOk: false, problems: 'plans invalides' },
|
||||
},
|
||||
{
|
||||
node: 'BUILD_FORECAST_URL',
|
||||
id: 'url-kill-switch',
|
||||
label: 'Kill-switch à OFF : aucune URL construite',
|
||||
snapshot: { enabled: 'off', api_key: 'ABCD1234', planes: '40/-45/4.03', latitude: 48.7, longitude: -3.5 },
|
||||
expect: { urlOk: false },
|
||||
},
|
||||
{
|
||||
node: 'BUILD_FORECAST_URL',
|
||||
id: 'url-trois-plans',
|
||||
label: 'Trois plans valides : URL construite, triplets préservés',
|
||||
snapshot: {
|
||||
enabled: 'on', api_key: 'ABCD1234',
|
||||
planes: '40/-45/4.03/35/0/9.3/45/90/2.79',
|
||||
latitude: 48.7732657, longitude: -3.5187179,
|
||||
},
|
||||
expect: { urlOk: true, planeCount: 3, urlTail: '40/-45/4.03/35/0/9.3/45/90/2.79' },
|
||||
},
|
||||
{
|
||||
node: 'BUILD_HA_CALLS_FORECAST',
|
||||
id: 'forecast-perimetre-respecte',
|
||||
label: 'Publication de la prévision : périmètre evcs_balance_* respecté',
|
||||
snapshot: FS_OK,
|
||||
expect: { outOfScope: '', callCount: 4, hasLogbook: true },
|
||||
},
|
||||
{
|
||||
node: 'BUILD_HA_CALLS_FORECAST',
|
||||
id: 'forecast-echec-ne-detruit-rien',
|
||||
label: 'Échec de récupération : la prévision précédente n\'est pas écrasée',
|
||||
snapshot: { ok: false, problems: ['réponse illisible ou sans résultat'] },
|
||||
expect: { outOfScope: '', wroteForecast: false },
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user