Chantier etapes 5b et 5c : recalcul quotidien des profils et fenetre de deficit suivante
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
This commit is contained in:
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { scenarios as censorshipScenarios } from './scenarios.mjs';
|
||||
import { profileScenarios, resourceScenarios } from './scenarios-profile.mjs';
|
||||
import { balanceScenarios } from './scenarios-balance.mjs';
|
||||
import { shadowScenarios } from './scenarios-shadow.mjs';
|
||||
import { shadowScenarios, profilesShadowScenarios } from './scenarios-shadow.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
|
||||
@@ -36,7 +36,7 @@ function runNode(nodeName, snapshot) {
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
const out = vm.runInContext(`(function(){ ${codeCache.get(nodeName)} })()`, sandbox, { timeout: 15000 });
|
||||
return nodeName === 'BUILD_HA_CALLS' ? out : out[0].json;
|
||||
return nodeName.startsWith('BUILD_HA_CALLS') ? out : out[0].json;
|
||||
}
|
||||
|
||||
const SUMMARIZE = {
|
||||
@@ -47,7 +47,7 @@ const SUMMARIZE = {
|
||||
healthOk: o.health.ok,
|
||||
alerts: o.alerts.join('|'),
|
||||
}),
|
||||
CONSUMPTION_PROFILE: o => ({
|
||||
DAILY_PROFILES: o => ({
|
||||
days: o.days,
|
||||
complete: o.complete,
|
||||
hours: o.profile.length,
|
||||
@@ -72,6 +72,11 @@ const SUMMARIZE = {
|
||||
gridNeeded: o.coverage.gridNeeded,
|
||||
prudentRequiredSocPct: o.coverage.prudent.requiredSocPct,
|
||||
deficitTrustworthy: o.confidence.deficitTrustworthy,
|
||||
nextDeficitFound: o.nextDeficit.found,
|
||||
nextDeficitStartHour: o.nextDeficit.found ? o.nextDeficit.startHour : null,
|
||||
nextDeficitEnergyKwh: o.nextDeficit.found ? o.nextDeficit.energyKwh : null,
|
||||
nextDeficitRequiredSocPct: o.nextDeficit.found ? o.nextDeficit.requiredSocAtStartPct : null,
|
||||
storageShortfallKwh: o.nextDeficit.found ? Math.round(o.nextDeficit.storageShortfallKwh * 100) / 100 : null,
|
||||
}) : ({ usable: false, missing: o.missing.join(',') }),
|
||||
};
|
||||
|
||||
@@ -86,8 +91,10 @@ SUMMARIZE.BUILD_HA_CALLS = (calls) => {
|
||||
wroteRequiredSoc: entities.includes('input_number.evcs_balance_required_soc'),
|
||||
wroteGridNeeded: entities.includes('input_boolean.evcs_balance_grid_needed'),
|
||||
hasLogbook: calls.some(c => c.json.domain === 'logbook'),
|
||||
wroteProfiles: entities.includes('input_text.evcs_balance_consumption_p50'),
|
||||
};
|
||||
};
|
||||
SUMMARIZE.BUILD_HA_CALLS_PROFILES = SUMMARIZE.BUILD_HA_CALLS;
|
||||
|
||||
const all = [
|
||||
...censorshipScenarios.map(s => ({ node: 'CENSORSHIP', ...s })),
|
||||
@@ -95,6 +102,7 @@ const all = [
|
||||
...resourceScenarios,
|
||||
...balanceScenarios,
|
||||
...shadowScenarios,
|
||||
...profilesShadowScenarios,
|
||||
];
|
||||
|
||||
let failed = 0;
|
||||
|
||||
@@ -97,6 +97,38 @@ export const balanceScenarios = [
|
||||
gridNeeded: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'bilan-prochaine-fenetre-deficit',
|
||||
label: 'Projection à 15 h : le solaire couvre déjà, mais la nuit qui vient doit être annoncée',
|
||||
snapshot: {
|
||||
nowMs: Date.parse('2026-08-24T15:30:00+02:00'),
|
||||
consumption_p50: P50,
|
||||
consumption_p80: P80,
|
||||
horizon: horizonFrom('2026-08-24T15:30:00+02:00', 24, {
|
||||
...PROD_24_08,
|
||||
15: 3.00, 16: 2.80, 17: 2.60, 18: 2.40, 19: 1.20, 20: 0.20,
|
||||
// lendemain matin
|
||||
7: 1.50, 8: 3.00, 9: 5.00,
|
||||
}),
|
||||
soc_percent: 95,
|
||||
usable_capacity_kwh: 15,
|
||||
soc_floor_percent: 22,
|
||||
},
|
||||
expect: {
|
||||
usable: true,
|
||||
// fenêtre en cours : le solaire couvre, rien à franchir
|
||||
gridNeeded: false,
|
||||
// mais la nuit qui vient doit être chiffrée
|
||||
nextDeficitFound: true,
|
||||
nextDeficitStartHour: 19,
|
||||
nextDeficitEnergyKwh: 12.77,
|
||||
// Au-dessus de 100 : même pleine, la batterie actuelle ne passe pas la
|
||||
// nuit sur le profil prudent. C'est le chiffre du dimensionnement.
|
||||
nextDeficitRequiredSocPct: 107.1,
|
||||
storageShortfallKwh: 1.07,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'bilan-entrees-manquantes',
|
||||
|
||||
@@ -31,7 +31,7 @@ function syntheticSeries({ days = 10, houseKwhPerHour = 1, evcsKwhAt14h = 3 } =
|
||||
|
||||
export const profileScenarios = [
|
||||
{
|
||||
node: 'CONSUMPTION_PROFILE',
|
||||
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 },
|
||||
@@ -45,7 +45,7 @@ export const profileScenarios = [
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'CONSUMPTION_PROFILE',
|
||||
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 },
|
||||
@@ -56,7 +56,7 @@ export const profileScenarios = [
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'CONSUMPTION_PROFILE',
|
||||
node: 'DAILY_PROFILES',
|
||||
id: 'compteur-remis-a-zero',
|
||||
label: 'Remise à zéro du compteur (redémarrage HA) : l\'échantillon est jeté, pas inventé',
|
||||
snapshot: (() => {
|
||||
@@ -74,7 +74,7 @@ export const profileScenarios = [
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'CONSUMPTION_PROFILE',
|
||||
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 },
|
||||
|
||||
@@ -79,3 +79,32 @@ export const shadowScenarios = [
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// --- 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 },
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user