5c — BALANCE ne repondait qu'a "combien jusqu'a ce que le solaire prenne le relais". A 15 h la reponse est zero : exacte mais inutile, le deficit qui approche etant celui de la nuit. Ajout de la prochaine traversee deficitaire (heure de debut, energie, SOC a atteindre avant). Ce SOC n'est PAS ecrete a 100 % : au-dela, meme une batterie pleine ne passe pas, et le manque est publie comme storage_shortfall_kwh — c'est le chiffre du dimensionnement. 5b — Workflow "EVCS Energy Profiles Daily V1" (N6UaB4ZozsbRPrFb), actif, 03:30 : lit /api/history/period sur 30 jours glissants et republie les quatre CSV (conso p50 et p80, enveloppe de production, taux de censure). CONSUMPTION_PROFILE devient DAILY_PROFILES et sait desormais decoder la reponse brute de l'API en plus des series deja pretes. Piege trouve : sans end_time, /api/history/period ne renvoie qu'UNE journee, silencieusement. Mes premieres mesures "sur 3 jours" n'en couvraient qu'une. Validation croisee : profils calcules depuis l'API REST (etats bruts, 11 j) contre profils calcules depuis les statistiques long terme (28 j) — 0,20 kWh/h d'ecart absolu moyen, par deux chemins de donnees independants. BUILD_HA_CALLS_PROFILES refuse d'ecraser les profils en place si l'historique ne couvre pas les 24 heures. Meme garantie de perimetre que le bilan. Suite a 27 scenarios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
145 lines
4.8 KiB
JavaScript
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: '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,
|
|
},
|
|
},
|
|
];
|