`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
194 lines
6.2 KiB
JavaScript
194 lines
6.2 KiB
JavaScript
// Scénarios de BUILD_HA_CALLS — garantie du mode ombre.
|
|
// La suite doit ÉCHOUER si une écriture sort du périmètre `evcs_balance_*`.
|
|
// C'est la seule protection structurelle contre un câblage réel accidentel.
|
|
|
|
const SNAPSHOT = {
|
|
balance_enabled: 'on',
|
|
grid_soc_off: '30.0',
|
|
soc_percent: '21',
|
|
usable_capacity_kwh: '15',
|
|
};
|
|
|
|
const CENSORSHIP_CENSURE = {
|
|
censorship: { pvCensored: true, reasons: ['batterie_pleine_sans_puits'] },
|
|
};
|
|
const CENSORSHIP_LIBRE = {
|
|
censorship: { pvCensored: false, reasons: [] },
|
|
};
|
|
|
|
const BALANCE_OK = {
|
|
usable: true,
|
|
crossing: { found: true, hour: 8 },
|
|
coverage: {
|
|
energyToCoverKwh: 0.82,
|
|
requiredSocPct: 27.5,
|
|
currentSocPct: 21,
|
|
socMarginPct: -6.5,
|
|
gridNeeded: true,
|
|
gridEnergyNeededKwh: 0.97,
|
|
prudent: { requiredSocPct: 38.1 },
|
|
},
|
|
vehicle: { surplusAvailableKwh: 20.17 },
|
|
confidence: { deficitTrustworthy: true },
|
|
};
|
|
|
|
// Le même bilan, mais sans croisement trouvé sur l'horizon : le SOC requis
|
|
// n'est plus une exigence datée, seulement la somme d'un horizon entier. Le
|
|
// statut doit le dire.
|
|
const BALANCE_SANS_CROISEMENT = {
|
|
...BALANCE_OK,
|
|
crossing: { found: false, hour: null },
|
|
coverage: { ...BALANCE_OK.coverage, requiredSocPct: 100, energyToCoverKwh: 25.97 },
|
|
};
|
|
|
|
export const shadowScenarios = [
|
|
{
|
|
node: 'BUILD_HA_CALLS',
|
|
id: 'ombre-perimetre-respecte',
|
|
label: 'Bilan complet : toutes les écritures restent dans evcs_balance_*',
|
|
snapshot: {
|
|
...BALANCE_OK,
|
|
__nodes: { 'HA Snapshot Balance': SNAPSHOT, CENSORSHIP: CENSORSHIP_CENSURE },
|
|
},
|
|
expect: {
|
|
outOfScope: '',
|
|
wroteRequiredSoc: true,
|
|
wroteGridNeeded: true,
|
|
hasLogbook: true,
|
|
status: 'ok',
|
|
},
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS',
|
|
id: 'ombre-sans-croisement-statut-degrade',
|
|
label:
|
|
'Aucun croisement sur l\'horizon : les chiffres sont publiés mais le statut passe à `degrade`',
|
|
snapshot: {
|
|
...BALANCE_SANS_CROISEMENT,
|
|
__nodes: { 'HA Snapshot Balance': SNAPSHOT, CENSORSHIP: CENSORSHIP_LIBRE },
|
|
},
|
|
expect: {
|
|
outOfScope: '',
|
|
wroteRequiredSoc: true,
|
|
status: 'degrade',
|
|
},
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS',
|
|
id: 'ombre-bilan-indisponible',
|
|
label: 'Bilan inutilisable : statut publié, aucune valeur inventée',
|
|
snapshot: {
|
|
usable: false,
|
|
missing: ['usable_capacity_kwh'],
|
|
__nodes: { 'HA Snapshot Balance': SNAPSHOT, CENSORSHIP: CENSORSHIP_LIBRE },
|
|
},
|
|
expect: {
|
|
outOfScope: '',
|
|
wroteRequiredSoc: false,
|
|
hasLogbook: false,
|
|
},
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS',
|
|
id: 'ombre-kill-switch',
|
|
label: 'Kill-switch à OFF : absolument aucune écriture',
|
|
snapshot: {
|
|
...BALANCE_OK,
|
|
__nodes: {
|
|
'HA Snapshot Balance': { ...SNAPSHOT, balance_enabled: 'off' },
|
|
CENSORSHIP: CENSORSHIP_CENSURE,
|
|
},
|
|
},
|
|
expect: {
|
|
callCount: 0,
|
|
outOfScope: '',
|
|
},
|
|
},
|
|
];
|
|
|
|
// --- BUILD_HA_CALLS_PROFILES (workflow quotidien) ------------------------
|
|
const PROFILES_OK = {
|
|
complete: true,
|
|
days: 28,
|
|
csv: { p50: '1,'.repeat(23) + '1', p80: '2,'.repeat(23) + '2',
|
|
envelope: '3,'.repeat(23) + '3', censorshipRate: '0,'.repeat(23) + '0' },
|
|
totals: { typicalDailyKwh: 31.9, prudentDailyKwh: 42.8 },
|
|
production: { demonstratedDailyKwh: 74.4, censoredDayHours: 8 },
|
|
quality: { hoursUsed: 639, pvHoursUsed: 620, countersReset: 0, unreliableHours: [] },
|
|
};
|
|
|
|
export const profilesShadowScenarios = [
|
|
{
|
|
node: 'BUILD_HA_CALLS_PROFILES',
|
|
id: 'profils-perimetre-respecte',
|
|
label: 'Publication des profils : périmètre evcs_balance_* respecté',
|
|
snapshot: PROFILES_OK,
|
|
expect: { outOfScope: '', callCount: 6, hasLogbook: true },
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS_PROFILES',
|
|
id: 'profils-historique-insuffisant',
|
|
label: 'Historique trop court : les profils en place ne sont PAS écrasés',
|
|
snapshot: { ...PROFILES_OK, complete: false, days: 2,
|
|
quality: { ...PROFILES_OK.quality, unreliableHours: [3, 4, 5] } },
|
|
expect: { outOfScope: '', wroteProfiles: false },
|
|
},
|
|
];
|
|
|
|
// --- Prévision solaire (workflow 15 min) ---------------------------------
|
|
const FS_OK = {
|
|
ok: true, days: 6,
|
|
today: { date: '2026-08-25', csv: '0.00,'.repeat(23) + '1.00', totalKwh: 43.5 },
|
|
tomorrow: { date: '2026-08-26', csv: '0.00,'.repeat(23) + '2.00', totalKwh: 30.0 },
|
|
rateLimitRemaining: 293,
|
|
};
|
|
|
|
export const forecastScenarios = [
|
|
{
|
|
node: 'BUILD_FORECAST_URL',
|
|
id: 'url-refus-cle-absente',
|
|
label: 'Clé absente : refus explicite, aucun appel gaspillé sur le quota',
|
|
snapshot: { enabled: 'on', api_key: 'unknown', planes: '40/-45/4.03', latitude: 48.7, longitude: -3.5 },
|
|
expect: { urlOk: false, problems: 'clé API absente' },
|
|
},
|
|
{
|
|
node: 'BUILD_FORECAST_URL',
|
|
id: 'url-refus-plans-invalides',
|
|
label: 'Plans mal formés (nombre de valeurs non multiple de 3) : refus',
|
|
snapshot: { enabled: 'on', api_key: 'ABCD1234', planes: '40/-45', latitude: 48.7, longitude: -3.5 },
|
|
expect: { urlOk: false, problems: 'plans invalides' },
|
|
},
|
|
{
|
|
node: 'BUILD_FORECAST_URL',
|
|
id: 'url-kill-switch',
|
|
label: 'Kill-switch à OFF : aucune URL construite',
|
|
snapshot: { enabled: 'off', api_key: 'ABCD1234', planes: '40/-45/4.03', latitude: 48.7, longitude: -3.5 },
|
|
expect: { urlOk: false },
|
|
},
|
|
{
|
|
node: 'BUILD_FORECAST_URL',
|
|
id: 'url-trois-plans',
|
|
label: 'Trois plans valides : URL construite, triplets préservés',
|
|
snapshot: {
|
|
enabled: 'on', api_key: 'ABCD1234',
|
|
planes: '40/-45/4.03/35/0/9.3/45/90/2.79',
|
|
latitude: 48.7732657, longitude: -3.5187179,
|
|
},
|
|
expect: { urlOk: true, planeCount: 3, urlTail: '40/-45/4.03/35/0/9.3/45/90/2.79' },
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS_FORECAST',
|
|
id: 'forecast-perimetre-respecte',
|
|
label: 'Publication de la prévision : périmètre evcs_balance_* respecté',
|
|
snapshot: FS_OK,
|
|
expect: { outOfScope: '', callCount: 4, hasLogbook: true },
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS_FORECAST',
|
|
id: 'forecast-echec-ne-detruit-rien',
|
|
label: 'Échec de récupération : la prévision précédente n\'est pas écrasée',
|
|
snapshot: { ok: false, problems: ['réponse illisible ou sans résultat'] },
|
|
expect: { outOfScope: '', wroteForecast: false },
|
|
},
|
|
];
|