// 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 }, confidence: { deficitTrustworthy: true }, // La réserve d'orage fait partie de la sortie normale de BALANCE : elle doit // figurer ici, sinon la garantie de périmètre ne couvre pas ses écritures. storm: { level: 'orange', known: true, active: true, phenomena: ['Orages'], windowStartHour: null, reserveKwh: 2.42, reserveSocPct: 38.1, socMarginPct: -17.1, covered: false, shortfallKwh: 2.57, }, }; // Le même bilan, mais sans croisement trouvé sur l'horizon : le SOC requis // n'est plus une exigence datée, seulement la somme d'un horizon entier. Le // statut doit le dire. const BALANCE_SANS_CROISEMENT = { ...BALANCE_OK, crossing: { found: false, hour: null }, coverage: { ...BALANCE_OK.coverage, requiredSocPct: 100, energyToCoverKwh: 25.97 }, }; 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, // Les 5 écritures de la réserve d'orage sont émises et restent dans le // périmètre ombre. wroteStorm: 5, hasLogbook: true, status: 'ok', }, }, { node: 'BUILD_HA_CALLS', id: 'ombre-sans-croisement-statut-degrade', label: 'Aucun croisement sur l\'horizon : les chiffres sont publiés mais le statut passe à `degrade`', snapshot: { ...BALANCE_SANS_CROISEMENT, __nodes: { 'HA Snapshot Balance': SNAPSHOT, CENSORSHIP: CENSORSHIP_LIBRE }, }, expect: { outOfScope: '', wroteRequiredSoc: true, status: 'degrade', }, }, { 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 }, }, ];