Nuit du 07 au 08/09 : 32 calculs du bilan sur 120 en statut degrade, tous entre 00 h et 03 h, avec « aucun croisement PV/conso sur l'horizon, 47,6 kWh a couvrir ». Le controleur, qui exige statut=ok, retombait sur le palier de 35 % — d'ou une cible du matin oscillant entre 35, 78,8 et 85 % au lieu de decroitre. Cause immediate : plus aucune publication de prevision de 23:45 a 04:30. Le noeud d'appel porte neverError, donc les executions restaient en success ; la reponse etait « socket hang up », et le noeud d'ecriture vers Home Assistant rendait 0 item la nuit contre 1 en journee — le .17 etait lui aussi injoignable, si bien que le message d'echec ne pouvait meme pas etre publie. Mais le defaut qu'on controle est ailleurs : les courbes sont publiees sans leur date, et PARSE_FORECAST prend days[0] pour « aujourd'hui ». Vrai a l'instant de la publication, faux des qu'on passe minuit sans rafraichir. A 00:45 le bilan a donc lu comme journee en cours celle du 7, dont toutes les heures etaient passees. Une courbe est desormais utilisable si elle DECRIT le jour dont on a besoin, pas si elle est recente : une prevision publiee a 23:45 pour le 8 reste exacte a 03:00, la nuit il n'y a rien a reactualiser. La date part dans evcs_balance_forecast_day0, et RESOURCE_ESTIMATE distingue trois cas — a jour, decalee d'un jour (la courbe du lendemain devient celle du jour, l'enveloppe mesuree reprend la main pour demain), perimee. Helper absent : ancienne garde d'age, comportement inchange. Le rapport de nuit signale un trou de plus de deux heures dans les publications. Il lit la base sur l'autre NAS, seul temoin qui survit quand le .17 est injoignable. Verifie sur la nuit ecoulee : 285 minutes. Tests 44 -> 48. Les quatre scenarios partagent la meme heure — 00:45, l'instant du decrochage — et les memes courbes : seule la date change. Quatre mutations, chacune sur un jeu different, chaque scenario ayant la sienne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
206 lines
6.8 KiB
JavaScript
206 lines
6.8 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 },
|
|
// La réserve d'orage fait partie de la sortie normale de BALANCE : elle doit
|
|
// figurer ici, sinon la garantie de périmètre ne couvre pas ses écritures.
|
|
storm: {
|
|
level: 'orange', known: true, active: true, phenomena: ['Orages'],
|
|
windowStartHour: null, reserveKwh: 2.42, reserveSocPct: 38.1,
|
|
socMarginPct: -17.1, covered: false, shortfallKwh: 2.57,
|
|
},
|
|
};
|
|
|
|
// 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,
|
|
// Les 5 écritures de la réserve d'orage sont émises et restent dans le
|
|
// périmètre ombre.
|
|
wroteStorm: 5,
|
|
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,
|
|
// 5 et non 4 depuis le 08/09 : la date que decrit la courbe est publiee a
|
|
// cote des courbes elles-memes (`forecast_day0`).
|
|
expect: { outOfScope: '', callCount: 5, 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 },
|
|
},
|
|
];
|