Files
EVCS/tests/energy-balance/scenarios-profile.mjs
Tallagrand Thierry c35c39cb83 Chantier : le bilan lit la courbe horaire au lieu de l'enveloppe
RESOURCE_ESTIMATE consomme desormais la courbe horaire publiee par le workflow
de prevision : forme et magnitude d'un seul tenant, sur 6 jours, avec les trois
plans reels et sans censure. Plus de repartition au prorata, plus de
renormalisation — le noeud se simplifie.

L'enveloppe est retrogradee en repli, utilisee seulement si la courbe est
absente ou vieille de plus d'une heure, le champ `mode` disant laquelle a
servi. Elle garde un role de repere de vraisemblance mais n'ecrete PLUS la
prevision : etant minoree l'apres-midi, elle tronquerait a tort une prevision
correcte.

Piege corrige au passage : le nouveau champ `source` entrait en collision avec
l'objet `source` deja present dans la sortie et etait silencieusement ecrase.
Renomme en `mode`.

Suite portee a 35 scenarios (courbe fraiche lue telle quelle, courbe perimee
basculant en repli).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-08-25 02:37:41 +02:00

186 lines
6.4 KiB
JavaScript

// 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-courbe-horaire',
label: 'Courbe horaire fraîche : lue telle quelle, sans répartition ni renormalisation',
snapshot: {
nowMs: Date.parse('2026-08-25T09:30:00+02:00'),
forecast_today_remaining_kwh: '999',
forecast_tomorrow_kwh: '999',
forecast_updated: '25/08 09:30',
// Relevé réel du 25/08 : 43,6 kWh sur la journée.
forecast_today_csv: '0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.07,0.63,1.60,3.50,6.50,7.90,7.00,4.60,3.30,2.70,2.20,1.70,1.10,0.60,0.10,0.00,0.00',
forecast_tomorrow_csv: '0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.09,0.77,1.60,2.27,2.81,3.63,4.24,3.52,2.95,2.59,2.21,1.69,1.08,0.47,0.05,0.00,0.00',
envelope_kwh: envelope.envelope_kwh,
censorship_rate: envelope.censorship_rate,
},
expect: {
usable: true,
mode: 'courbe_horaire_api',
// Reste de la journée à partir de 09 h : lu dans la courbe, pas recalculé.
todayExpected: 42.8,
},
},
{
node: 'RESOURCE_ESTIMATE',
id: 'ressource-courbe-perimee',
label: 'Courbe vieille de plus d\'une heure : repli sur l\'enveloppe, signalé',
snapshot: {
nowMs: Date.parse('2026-08-25T12:30:00+02:00'),
forecast_today_remaining_kwh: '15.9',
forecast_tomorrow_kwh: '29.1',
forecast_updated: '25/08 09:30',
forecast_today_csv: '0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.07,0.63,1.60,3.50,6.50,7.90,7.00,4.60,3.30,2.70,2.20,1.70,1.10,0.60,0.10,0.00,0.00',
envelope_kwh: envelope.envelope_kwh,
censorship_rate: envelope.censorship_rate,
},
expect: {
usable: true,
mode: 'repli_enveloppe',
todayExpected: 15.9,
},
},
{
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,
},
},
];