Files
EVCS/tests/energy-balance/scenarios-profile.mjs
Tallagrand Thierry 4b7cc3ae18 Chantier etape 2 : profil horaire de consommation mesure
CONSUMPTION_PROFILE.js : compteurs d'energie cumules -> deltas horaires ->
distribution par heure de la journee -> percentiles. La consommation du
vehicule est retiree : c'est une charge pilotable, elle ne doit pas polluer le
profil de la charge subie. Les remises a zero de compteur et les deltas
implausibles sont jetes, jamais rattrapes par une valeur inventee, et une heure
sans assez de jours observes est marquee non fiable au lieu d'etre publiee en
silence.

Valide sur 28 jours reels (639 heures, 0 reset) : 31,9 kWh/jour typique hors
vehicule, 42,8 kWh au p80. Le profil tient dans un helper input_text
(119 caracteres sur 255).

Verification prealable de la methode : seulement 7,8 % des heures de deficit
sont censurees. Censure et deficit sont physiquement presque exclusifs, donc la
grandeur qui pilote la decision EDF est mesuree sur des donnees propres.

Suite tests/energy-balance/ etendue aux deux noeuds : 14 scenarios.

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

87 lines
3.0 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',
},
},
];