Files
EVCS/tests/energy-balance/scenarios-shadow.mjs
Tallagrand Thierry 26fb9f72ea 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
2026-08-25 02:30:28 +02:00

168 lines
5.3 KiB
JavaScript

// Scénarios de BUILD_HA_CALLS — garantie du mode ombre.
// La suite doit ÉCHOUER si une écriture sort du périmètre `evcs_balance_*`.
// C'est la seule protection structurelle contre un câblage réel accidentel.
const SNAPSHOT = {
balance_enabled: 'on',
grid_soc_off: '30.0',
soc_percent: '21',
usable_capacity_kwh: '15',
};
const CENSORSHIP_CENSURE = {
censorship: { pvCensored: true, reasons: ['batterie_pleine_sans_puits'] },
};
const CENSORSHIP_LIBRE = {
censorship: { pvCensored: false, reasons: [] },
};
const BALANCE_OK = {
usable: true,
crossing: { found: true, hour: 8 },
coverage: {
energyToCoverKwh: 0.82,
requiredSocPct: 27.5,
currentSocPct: 21,
socMarginPct: -6.5,
gridNeeded: true,
gridEnergyNeededKwh: 0.97,
prudent: { requiredSocPct: 38.1 },
},
vehicle: { surplusAvailableKwh: 20.17 },
};
export const shadowScenarios = [
{
node: 'BUILD_HA_CALLS',
id: 'ombre-perimetre-respecte',
label: 'Bilan complet : toutes les écritures restent dans evcs_balance_*',
snapshot: {
...BALANCE_OK,
__nodes: { 'HA Snapshot Balance': SNAPSHOT, CENSORSHIP: CENSORSHIP_CENSURE },
},
expect: {
outOfScope: '',
wroteRequiredSoc: true,
wroteGridNeeded: true,
hasLogbook: true,
},
},
{
node: 'BUILD_HA_CALLS',
id: 'ombre-bilan-indisponible',
label: 'Bilan inutilisable : statut publié, aucune valeur inventée',
snapshot: {
usable: false,
missing: ['usable_capacity_kwh'],
__nodes: { 'HA Snapshot Balance': SNAPSHOT, CENSORSHIP: CENSORSHIP_LIBRE },
},
expect: {
outOfScope: '',
wroteRequiredSoc: false,
hasLogbook: false,
},
},
{
node: 'BUILD_HA_CALLS',
id: 'ombre-kill-switch',
label: 'Kill-switch à OFF : absolument aucune écriture',
snapshot: {
...BALANCE_OK,
__nodes: {
'HA Snapshot Balance': { ...SNAPSHOT, balance_enabled: 'off' },
CENSORSHIP: CENSORSHIP_CENSURE,
},
},
expect: {
callCount: 0,
outOfScope: '',
},
},
];
// --- BUILD_HA_CALLS_PROFILES (workflow quotidien) ------------------------
const PROFILES_OK = {
complete: true,
days: 28,
csv: { p50: '1,'.repeat(23) + '1', p80: '2,'.repeat(23) + '2',
envelope: '3,'.repeat(23) + '3', censorshipRate: '0,'.repeat(23) + '0' },
totals: { typicalDailyKwh: 31.9, prudentDailyKwh: 42.8 },
production: { demonstratedDailyKwh: 74.4, censoredDayHours: 8 },
quality: { hoursUsed: 639, pvHoursUsed: 620, countersReset: 0, unreliableHours: [] },
};
export const profilesShadowScenarios = [
{
node: 'BUILD_HA_CALLS_PROFILES',
id: 'profils-perimetre-respecte',
label: 'Publication des profils : périmètre evcs_balance_* respecté',
snapshot: PROFILES_OK,
expect: { outOfScope: '', callCount: 6, hasLogbook: true },
},
{
node: 'BUILD_HA_CALLS_PROFILES',
id: 'profils-historique-insuffisant',
label: 'Historique trop court : les profils en place ne sont PAS écrasés',
snapshot: { ...PROFILES_OK, complete: false, days: 2,
quality: { ...PROFILES_OK.quality, unreliableHours: [3, 4, 5] } },
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 },
},
];