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:
Tallagrand Thierry
2026-08-24 15:58:10 +02:00
parent e36f050a42
commit 6dc5ea25cb
14 changed files with 668 additions and 22 deletions

View File

@@ -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;