Files
EVCS/tests/energy-balance/scenarios-profile.mjs
Tallagrand Thierry 180a1f3db5 Chantier etape 3 : estimateur de ressource PV, apres deux approches ecartees
RESOURCE_ESTIMATE.js. Recalage de la prevision : ecarte, la censure est
correlee au beau temps (facteur 4,29 a 9 h sur 27 echantillons contre 0,48 a
15 h sur 3), les donnees manquantes ne le sont pas au hasard. Forme
geometrique : ecartee, un plan unique ajuste sur les heures propres converge
sur plein est 20 degres et predit 2,0 % de la journee a 18 h contre 5,8 %
mesures — le champ a plusieurs orientations que ce modele ne represente pas.

Retenu : forme = enveloppe horaire demontree (comportement reel du champ, sans
modele), magnitude = prevision Forecast.Solar (meteo, insensible a notre
batterie). L'enveloppe de l'apres-midi etant elle-meme censuree, le noeud
publie un indice de confiance et signale les heures minorees au lieu de les
presenter comme fiables. Sans enveloppe, il se declare inutilisable plutot que
d'inventer une forme.

Defaut trouve au passage : l'orientation declaree a Forecast.Solar ne
correspond pas au champ. Matin 07-11 h, 15,1 % de la journee prevus contre
39,3 % reels ; apres-midi 14-18 h, 52,1 % contre 35,2 %. Les totaux
journaliers tiennent, la repartition intra-journaliere est inutilisable.

Suite portee a 17 scenarios.

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

145 lines
4.8 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: 'CONSUMPTION_PROFILE',
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: 'CONSUMPTION_PROFILE',
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: 'CONSUMPTION_PROFILE',
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: 'CONSUMPTION_PROFILE',
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,
},
},
];