// Scénarios du nœud CONSUMPTION_PROFILE. // `fixture-history.json` = 28 jours réels (28/07 → 24/08/2026), un point horaire // par compteur, au format renvoyé par /api/history. import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const history = JSON.parse(fs.readFileSync(path.join(HERE, 'fixture-history.json'), 'utf8')); const NOW = Date.parse('2026-08-24T14:00:00Z'); // Petit historique synthétique : 10 jours, conso maison constante de 1 kWh/h, // et une charge de véhicule de 3 kWh à 14 h tous les jours. function syntheticSeries({ days = 10, houseKwhPerHour = 1, evcsKwhAt14h = 3 } = {}) { const inverter = []; const evcs = []; let cumInverter = 0; let cumEvcs = 0; const start = Date.parse('2026-08-01T00:00:00+02:00'); for (let h = 0; h < days * 24; h++) { const ts = new Date(start + h * 3600000); const isEvcsHour = ts.getUTCHours() === 12; // 14 h Paris en août cumInverter += houseKwhPerHour + (isEvcsHour ? evcsKwhAt14h : 0); cumEvcs += isEvcsHour ? evcsKwhAt14h : 0; inverter.push({ ts: ts.toISOString(), state: String(cumInverter) }); evcs.push({ ts: ts.toISOString(), state: String(cumEvcs) }); } return { inverter_to_ac_out: inverter, ac_in_to_ac_out: [], evcs_total: evcs }; } export const profileScenarios = [ { node: 'DAILY_PROFILES', id: 'profil-historique-reel', label: '28 jours réels : profil complet sur 24 heures, aucun compteur remis à zéro', snapshot: { series: history.series, nowMs: NOW }, expect: { days: 28, complete: true, hours: 24, countersReset: 0, unreliableHours: '', csvFitsHelper: true, }, }, { node: 'DAILY_PROFILES', id: 'evcs-retire-du-profil', label: 'La charge du véhicule est retirée : le profil reste plat à 1 kWh/h, y compris à 14 h', snapshot: { series: syntheticSeries(), nowMs: NOW }, expect: { p50AtHour14: 1, p80AtHour14: 1, complete: true, }, }, { node: 'DAILY_PROFILES', id: 'compteur-remis-a-zero', label: 'Remise à zéro du compteur (redémarrage HA) : l\'échantillon est jeté, pas inventé', snapshot: (() => { const s = syntheticSeries(); // 5e jour à 10 h : le compteur repart de zéro. const idx = 4 * 24 + 10; for (let i = idx; i < s.inverter_to_ac_out.length; i++) { s.inverter_to_ac_out[i].state = String(Number(s.inverter_to_ac_out[i].state) - 100); } return { series: s, nowMs: NOW }; })(), expect: { countersReset: 1, complete: true, }, }, { node: 'DAILY_PROFILES', id: 'historique-trop-court', label: '3 jours seulement : les heures sont marquées non fiables plutôt que publiées en silence', snapshot: { series: syntheticSeries({ days: 3 }), nowMs: NOW }, expect: { complete: false, unreliableHours: '0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23', }, }, ]; // --- RESOURCE_ESTIMATE ------------------------------------------------- const envelope = JSON.parse(fs.readFileSync(path.join(HERE, 'fixture-envelope.json'), 'utf8')); const MIDDAY = Date.parse('2026-08-24T12:30:00+02:00'); export const resourceScenarios = [ { node: 'RESOURCE_ESTIMATE', id: 'ressource-repartition-prevision', label: 'Prévision du jour répartie sur la forme démontrée, sans dépasser le total prévu', snapshot: { nowMs: MIDDAY, forecast_today_remaining_kwh: '15.9', forecast_tomorrow_kwh: '29.1', envelope_kwh: envelope.envelope_kwh, censorship_rate: envelope.censorship_rate, }, expect: { usable: true, todayExpected: 15.9, tomorrowExpected: 29.1, // Le milieu et la fin de journée reposent sur une forme minorée : // la confiance doit s'effondrer, pas rester silencieusement à 1. shapeConfidenceBelow: true, }, }, { node: 'RESOURCE_ESTIMATE', id: 'ressource-matin-forme-fiable', label: 'Projection faite au petit matin : la forme du matin est propre, la confiance doit être bonne', snapshot: { nowMs: Date.parse('2026-08-24T06:30:00+02:00'), forecast_today_remaining_kwh: '30', forecast_tomorrow_kwh: '0', envelope_kwh: envelope.envelope_kwh, censorship_rate: envelope.censorship_rate, }, expect: { usable: true, todayExpected: 30, }, }, { node: 'RESOURCE_ESTIMATE', id: 'ressource-sans-enveloppe', label: 'Aucune enveloppe fournie : le nœud se déclare inutilisable au lieu d\'inventer une forme', snapshot: { nowMs: MIDDAY, forecast_today_remaining_kwh: '15.9', forecast_tomorrow_kwh: '29.1', }, expect: { usable: false, todayExpected: 0, tomorrowExpected: 0, }, }, ];