La cible du matin devient une energie a couvrir, plus un palier

La regle classait le lendemain en trois paliers a partir de son TOTAL (>= 18 kWh
-> 35 %, >= 10 -> 55 %, sinon 85 %) et ignorait donc l heure d arrivee de
l energie. Le 03/09 demain annoncait 33,9 kWh — donc "fort", donc 35 % — avec un
matin creux (0,03 kWh a 7 h, 0,77 a 8 h) et un croisement a 9 h. Meme defaut que
le plancher du soir, a l autre bout de la nuit.

Releve de la nuit 02-03/09, requis contre reel : 00:30 62,4 / 53 ; 03:00
50,4 / 45 ; 04:30 41,7 / 37 ; 06:00 37,1 / 22 — cible fixe a 35 tout du long.
Le required_soc decroit proprement avec la nuit qui se consomme (6,1 -> 0 kWh a
couvrir), la batterie est restee sous la ligne toute la nuit, et le bilan le
savait des 00:30. A 03:00 EDF a ete relache cinq points sous la ligne ; le
reseau est revenu de 07:24 a 10:33 au plein tarif, 1,49 kWh.

La cible vaut desormais min(90, max(palier ou advisor, required_soc)). Sens
unique delibere : la direction corrigee est celle de l echec observe, et le
principe unificateur des deux cablages est que le bilan ne pousse jamais dans le
sens "couvre moins bien la nuit". Plafond 90 parce qu au-dela la batterie cesse
d etre un puits et evince la production du matin, et parce que le bilan publie
sans ecretage des valeurs superieures a 100 — sans ce plafond la cible serait
inatteignable et EDF resterait allume toute la nuit. C est deja la borne du
chemin advisor, aucune constante nouvelle.

Pas d oscillateur : required_soc decroit de facon monotone pendant la nuit et la
passe unique de recovery reste en place. Gardes et repli identiques au plancher
du soir, kill-switch existant.

Golden 70 -> 75. Quatre mutations font tomber chacune exactement un scenario
different. Un cinquieme scenario fige une LIMITE CONNUE, mesuree par sonde : la
passe unique de recovery latche le relachement pour toute la session, donc la
cible dynamique n ameliore que la premiere passe. §26 avait deja designe le bon
critere de re-engagement — un budget d energie restant a couvrir, qui appartient
au bilan ; la suite touche la machine a etats de la nuit et fera l objet d un
chantier propre.

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-09-03 22:51:09 +02:00
parent 6f3f98e8b6
commit 05bdf019c2
7 changed files with 344 additions and 9 deletions

View File

@@ -361,6 +361,7 @@ const batteryMorningTargetPct =
? nightPrev.batteryMorningTargetPct
: params.evcsNightTargetWeakForecastPct;
const advisorNightTargetApplied = !!nightPrev.advisorNightTargetApplied;
const balanceMorningTargetApplied = !!nightPrev.balanceMorningTargetApplied;
const maxBatteryDischargePoints =
Number.isFinite(nightPrev.maxBatteryDischargePoints) ? nightPrev.maxBatteryDischargePoints : 0;
const gridTakeoverTriggerPct =
@@ -1080,6 +1081,7 @@ return [{
batteryHardFloorPct,
batteryMorningTargetPct,
advisorNightTargetApplied,
balanceMorningTargetApplied,
maxBatteryDischargePoints,
gridTakeoverTriggerPct,

View File

@@ -1319,7 +1319,7 @@ else {
// =====================================================
if (nightEvcs?.nightSessionActive) {
reason.push(
`Nuit EVCS: target=${round1(nightEvcs.batteryMorningTargetPct)}%${nightEvcs.advisorNightTargetApplied ? ' (ADVISOR)' : ''} | trigger réseau=${round1(nightEvcs.gridTakeoverTriggerPct)}%`
`Nuit EVCS: target=${round1(nightEvcs.batteryMorningTargetPct)}%${nightEvcs.advisorNightTargetApplied ? ' (ADVISOR)' : ''}${nightEvcs.balanceMorningTargetApplied ? ' (BILAN)' : ''} | trigger réseau=${round1(nightEvcs.gridTakeoverTriggerPct)}%`
);
if (Number.isFinite(nightEvcs.nightSessionStartSocPct)) {

View File

@@ -285,7 +285,7 @@ const advisorNightTargetApplied =
advisorTargetAgeMin !== null &&
advisorTargetAgeMin <= ADVISOR_TARGET_MAX_AGE_MIN;
const batteryMorningTargetPct =
const batteryMorningTargetBasePct =
advisorNightTargetApplied
? clamp(
params.advisorNightTargetSocPct,
@@ -294,6 +294,65 @@ const batteryMorningTargetPct =
)
: batteryMorningTargetRulePct;
// =====================================================
// CIBLE DU MATIN : UNE ENERGIE A COUVRIR, PAS UN PALIER (03/09/2026)
// =====================================================
// La regle historique classe la journee du lendemain en trois paliers
// (>= 18 kWh -> 35 %, >= 10 -> 55 %, sinon 85 %) a partir de son TOTAL. Elle
// est donc aveugle a l'heure d'arrivee de l'energie : le 03/09, demain
// annoncait 33,9 kWh — « fort », donc 35 % — mais avec un matin creux
// (0,03 kWh a 7 h, 0,77 a 8 h, 1,81 a 9 h) et un croisement a 9 h.
//
// C'est le meme defaut que le plancher du soir (cf. le bloc BILAN de
// BATTERY_CLOUD_LOCKS), a l'autre bout de la nuit : un palier fige contre une
// energie mesurable. Le bilan publie la seconde, instant par instant.
//
// RELEVE DE LA NUIT 02-03/09, qui a motive ce cablage :
//
// heure requis SOC reel cible fixe
// 00:30 62,4 53 35
// 03:00 50,4 45 35 <- EDF relache ici, 5 points sous la ligne
// 04:30 41,7 37 35
// 06:00 37,1 22 35 <- hysteresis, puis reseau PLEIN TARIF
// de 07:24 a 10:33 (1,49 kWh)
//
// La batterie est restee sous la ligne toute la nuit et le bilan le savait des
// 00:30. Le `required_soc` decroit proprement avec la nuit qui se consomme
// (6,1 -> 0 kWh a couvrir) : c'est bien une cible instantanee, pas une
// constante deguisee.
//
// SENS UNIQUE, comme pour le plancher du soir : le bilan ne peut que RELEVER
// la cible, jamais l'abaisser. La direction corrigee est celle de l'echec
// observe (cible trop basse -> batterie a plat avant le croisement -> reseau
// au plein tarif le matin). L'abaisser — la regle dit 85 % l'hiver quand le
// bilan dira parfois moins — ouvrirait un risque neuf que rien n'a encore
// valide. Le principe unificateur : le bilan ne pousse JAMAIS dans le sens
// « couvre moins bien la nuit ».
//
// PLAFOND 90 % : au-dela, la batterie cesse d'etre un puits et la production
// du matin se fait evincer (cf. mesures du 31/08). Charger au reseau au-dela
// de 90 echangerait un kWh bon marche cette nuit contre plusieurs kWh censures
// demain. C'est deja la borne du chemin advisor : aucune constante nouvelle.
//
// PAS D'OSCILLATEUR : `required_soc` decroit de facon monotone pendant la nuit
// (l'energie restant a couvrir ne fait que diminuer), et la passe unique de
// recovery (`nightRecoveryDonePrev`, 27/08) reste en place.
//
// REPLI identique au plancher du soir : fraicheur, statut, valeur finie —
// sinon regle historique. Kill-switch existant : `evcs_balance_enabled` OFF
// fige `balance_last_run` et la cible retombe seule sur les paliers.
const BALANCE_TARGET_MAX_AGE_MIN = 30;
const balanceTargetAgeMin = minutesSince(params.balanceLastRunMs, nowMs);
const balanceMorningTargetApplied =
params.balanceStatusOk &&
Number.isFinite(params.balanceRequiredSocPct) &&
balanceTargetAgeMin !== null &&
balanceTargetAgeMin <= BALANCE_TARGET_MAX_AGE_MIN;
const batteryMorningTargetPct = balanceMorningTargetApplied
? Math.min(90, Math.max(batteryMorningTargetBasePct, params.balanceRequiredSocPct))
: batteryMorningTargetBasePct;
const maxBatteryDischargePoints =
nightSessionActive && Number.isFinite(nightSessionStartSocPct)
? Math.max(0, nightSessionStartSocPct - batteryMorningTargetPct)
@@ -898,8 +957,13 @@ return [{
batteryHardFloorPct,
batteryMorningTargetPct,
batteryMorningTargetBasePct,
balanceMorningTargetApplied,
balanceTargetAgeMin:
balanceTargetAgeMin === null ? null : Math.round(balanceTargetAgeMin * 10) / 10,
batteryMorningTargetRulePct,
advisorNightTargetApplied,
balanceMorningTargetApplied,
maxBatteryDischargePoints,
gridTakeoverTriggerPct,