`bucketHourly` garde la derniere valeur de chaque heure : le delta entre H-1 et H est l'energie depensee PENDANT H. `deltaSeries` le rangeait sous H-1. Discret sur un compteur dense, catastrophique sur un compteur epars — et les compteurs de cette installation le sont : `ac_in_1_to_ac_out` ne peuple que 57 heures sur 267, `evcs_total` 57 sur 264, parce qu'ils n'emettent que lorsqu'ils avancent. Une heure vide vaut « zero energie », donc toute l'energie d'un intervalle a trou a ete consommee pendant l'heure FINALE. Rangee sous l'heure initiale, l'import reseau de la nuit suivante atterrissait sur la derniere heure de la nuit precedente. Mesure du 25/08 : consumption_p50[8] = 2,90 kWh/h pour 0,80 reellement — six echantillons sur douze portaient un delta etale sur 17 heures. Le bilan reclamait environ deux fois trop d'energie le matin (3,37 kWh annonces a 06 h contre 1,35 mesures), et donc un SOC requis de 44,5 % la ou 31 % suffisait. L'enveloppe de production etait decalee de la meme heure, ce qui a contribue a surestimer la part matinale de la production dans le journal de chantier. Ajoute aussi un statut `degrade` : le bilan publie ses chiffres quand aucun croisement n'apparait sur l'horizon ou quand les heures de deficit reposent sur une forme minoree, mais il ne les presente plus comme fiables. La nuit du 25/08, avant la mise en service de la courbe horaire, il a publie un SOC requis de 100 % avec un statut `ok`. Tests 36 -> 37, dont `compteur-epars-attribution-horaire` (rafale reseau de 1/2/3 kWh a 01/02/03 h : montants distincts pour qu'un decalage d'un cran soit impossible a confondre). Mutation verifiee : l'ancienne attribution le fait echouer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
235 lines
8.4 KiB
JavaScript
235 lines
8.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',
|
|
},
|
|
},
|
|
{
|
|
node: 'DAILY_PROFILES',
|
|
id: 'compteur-epars-attribution-horaire',
|
|
label:
|
|
'Compteur épars (réseau allumé seulement la nuit) : chaque kWh se range sous l\'heure qui l\'a consommé',
|
|
// Reproduit le défaut mesuré le 25/08/2026 : `ac_in_1_to_ac_out` n'émet que
|
|
// pendant les heures creuses. Entre deux rafales, le compteur reste muet
|
|
// pendant 21 heures. L'énergie d'un intervalle à trou appartient à l'heure
|
|
// qui a réveillé le compteur, pas à celle où il s'était endormi.
|
|
snapshot: (() => {
|
|
const inverter = [];
|
|
const grid = [];
|
|
let cumInverter = 0;
|
|
let cumGrid = 0;
|
|
const start = Date.parse('2026-08-01T00:00:00+02:00');
|
|
// Rafale réseau : 1 kWh à 01 h, 2 kWh à 02 h, 3 kWh à 03 h. Montants
|
|
// distincts pour qu'un décalage d'un cran soit impossible à confondre.
|
|
const burst = { 1: 1, 2: 2, 3: 3 };
|
|
for (let h = 0; h < 12 * 24; h++) {
|
|
const ts = new Date(start + h * 3600000);
|
|
const hourParis = Number.parseInt(
|
|
new Intl.DateTimeFormat('fr-FR', {
|
|
timeZone: 'Europe/Paris', hour: '2-digit', hour12: false,
|
|
}).format(ts),
|
|
10
|
|
);
|
|
cumInverter += 0.8;
|
|
inverter.push({ ts: ts.toISOString(), state: String(cumInverter.toFixed(2)) });
|
|
const kwh = burst[hourParis];
|
|
if (kwh) {
|
|
cumGrid += kwh;
|
|
// Le compteur n'émet QUE pendant les heures où il avance.
|
|
grid.push({ ts: ts.toISOString(), state: String(cumGrid.toFixed(2)) });
|
|
}
|
|
}
|
|
return {
|
|
series: { inverter_to_ac_out: inverter, ac_in_to_ac_out: grid, evcs_total: [] },
|
|
nowMs: NOW,
|
|
};
|
|
})(),
|
|
expect: {
|
|
// 0,8 de fond + la rafale de l'heure : 1,8 / 2,8 / 3,8.
|
|
p50Hours1a3: '1.8,2.8,3.8',
|
|
// 08 h ne consomme que le fond : c'est l'heure qui portait 2,90 kWh/h
|
|
// en production avant correction.
|
|
p50AtHour8: 0.8,
|
|
complete: true,
|
|
},
|
|
},
|
|
];
|
|
|
|
// --- 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,
|
|
},
|
|
},
|
|
];
|