Fenetre d etablissement : ne plus decider sur les mesures du transitoire

A la fermeture du contacteur, PV, import et courant batterie ne decrivent aucun
regime permanent pendant ~150 s (mesure du 31/08). La boucle de 30 s y prenait
cinq decisions, et la moyenne PV 10 min en gardait la trace dix minutes de plus
-- le systeme lisait "plus de soleil", en concluait qu il avait besoin du
reseau, et confirmait sa propre premisse.

Aucun seuil nouveau : le systeme publiait deja l horodatage de la transition et
personne ne le lisait. Pendant la fenetre, on ne demarre pas et on ne laisse pas
une charge s arreter sur ces mesures ; les step-down restent libres.

Deux asymetries voulues : la fenetre ne s ouvre qu a l ARRIVEE du reseau (a sa
disparition les onduleurs reprennent tout, les gardes doivent agir plus vite),
et elle suspend le confort, jamais le risque.

Golden 42 -> 48, chaque cas apparie a son contrefactuel hors fenetre. Mutation :
gridSettling force a false -> 2 echecs cibles, restauration -> 48/48.

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-31 18:18:03 +02:00
parent f07d4cb0f1
commit f0479945c4
8 changed files with 359 additions and 6 deletions

View File

@@ -563,8 +563,17 @@ const batteryAssistSolarHoldOk =
// =====================================================
// START / HOLD CONDITIONS
// =====================================================
// FENETRE D'ETABLISSEMENT (31/08/2026) — cf. WINDOWS_SOURCEV2 pour la mesure.
// Pendant les ~150 s qui suivent l'arrivee du reseau, PV et courant batterie ne
// mesurent rien d'exploitable. On NE DEMARRE PAS sur ces valeurs, et on ne
// laisse pas une charge en cours s'arreter a cause d'elles. Les gardes de
// securite (commonSafety*, planchers SOC, surcharge, pas de tampon) restent
// evaluees normalement : c'est le risque qui prime, jamais le confort.
const gridSettling = derived.gridSettling === true;
const rawSolarStartOk =
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
!gridSettling &&
commonSafetyStartOk &&
parsed.soc >= effectiveSocStartFloor &&
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
@@ -580,8 +589,12 @@ const rawSolarHoldOk =
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
commonSafetyHoldOk &&
parsed.soc >= effectiveSocHoldFloor &&
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
(
gridSettling ||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA
) &&
(
gridSettling ||
parsed.pvPowerW >= params.pvHoldMinW ||
cloudToleranceOk
);
@@ -667,9 +680,13 @@ const probeSuccessNow =
parsed.batteryCurrentA >= Math.min(0, batteryDischargeSoftDynamicA)
);
// Une sonde ne peut pas etre declaree ratee sur une mesure prise pendant
// l'etablissement : c'est le reseau qui masque la production, pas la sonde qui
// echoue. On differe le verdict, on ne l'invente pas.
const probeFailedNow =
probeModeActive &&
probeElapsedReached &&
!gridSettling &&
!probeSuccessNow;
// =====================================================
@@ -689,7 +706,10 @@ const stepUpByForecastMode =
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 5) &&
cloudToleranceOk;
// Pas de montee en courant sur des mesures d'etablissement. La descente, elle,
// n'est pas bridee : c'est la direction sure.
const solarShouldStepUp =
!gridSettling &&
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA < derived.physicalCapA &&
derived.physicalCapA >= params.currentMinA &&
@@ -906,6 +926,8 @@ return [{
solarStartOk,
solarHoldOk,
gridSettling,
gridTransitionAgeSec: parsed.gridTransitionAgeSec,
gridStartOk,
gridHoldOk,

View File

@@ -310,6 +310,12 @@ reason.push(`Batt=${round1(parsed.batteryCurrentA)}A`);
reason.push(`PV=${Math.round(parsed.pvPowerW)}W`);
reason.push(`PV10=${Math.round(parsed.pvPowerAvg10mW)}W`);
reason.push(`Grid=${parsed.gridAvailable ? 'on' : 'off'} (${Math.round(parsed.gridPowerW)}W)`);
// Trace de la fenetre d'etablissement : sans elle, une lecture du logbook
// pendant le transitoire donne l'impression que le systeme decide n'importe
// quoi. Il decide, au contraire, de ne PAS decider sur ces mesures.
if (derived.gridSettling) {
reason.push(`ETABLISSEMENT reseau ${Math.round(parsed.gridTransitionAgeSec)}s/${params.gridSettleSeconds}s (mesures PV/batterie non fiables)`);
}
reason.push(`Cap=${derived.physicalCapA}A`);
reason.push(`Ref=${round1(derived.currentReferenceA)}A`);
reason.push(`Target=${round1(desiredCurrentA)}A`);

View File

@@ -13,6 +13,11 @@
"grid_state": states("switch.edf_general_ok_switch"),
"victron_acin1_available": states("sensor.victron_ac_in1_available"),
"acin1_last_changed_ts": as_timestamp(states.sensor.victron_ac_in1_available.last_changed, 0) if states.sensor.victron_ac_in1_available is not none else 0,
"grid_switch_last_changed_ts": as_timestamp(states.switch.edf_general_ok_switch.last_changed, 0) if states.switch.edf_general_ok_switch is not none else 0,
"grid_settle_seconds": states("input_number.evcs_grid_settle_seconds"),
"today_remaining_kwh": states("sensor.energy_production_today_remaining"),
"tomorrow_kwh": states("sensor.energy_production_tomorrow"),

View File

@@ -249,6 +249,21 @@ const parsed = {
(low(input.grid_state || 'off') === 'on') ||
(num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1),
// FENETRE D'ETABLISSEMENT (31/08/2026) — age, en secondes, de la derniere
// transition du reseau. On retient la plus RECENTE des deux sources : la
// presence physique AC-IN (celle qui declenche reellement le transitoire) et
// le contacteur (qui peut bouger sans qu'AC-IN suive, disjoncteur ouvert).
// Horodatage absent ou nul => age tres grand => aucune fenetre, comportement
// historique. On ne devine jamais une transition qu'on n'a pas observee.
gridTransitionAgeSec: (() => {
const ts = Math.max(
num(input.acin1_last_changed_ts, 0),
num(input.grid_switch_last_changed_ts, 0)
);
if (!(ts > 0)) return Number.POSITIVE_INFINITY;
return Math.max(0, (nowMs / 1000) - ts);
})(),
todayRemainingKwh: Math.max(0, num(input.today_remaining_kwh, 0)),
tomorrowKwh: Math.max(0, num(input.tomorrow_kwh, 0)),
@@ -398,6 +413,12 @@ const params = {
advisorStatusOk: cleanText(input.advisor_status).toLowerCase() === 'ok',
advisorLastRunMs: parseAdvisorRunToMs(cleanText(input.advisor_last_run)),
// Duree de la fenetre d'etablissement. Valeur mesuree le 31/08/2026 : le
// transitoire de fermeture dure ~150 s (import 1985 -> 25 W et 2600 -> 52 W,
// deux campagnes), on prend 180 s de marge. Reglable sans redeploiement via
// input_number.evcs_grid_settle_seconds ; absent => 180.
gridSettleSeconds: clamp(num(input.grid_settle_seconds, 180), 0, 900),
probeEnabled: low(input.probe_enabled || 'off') === 'on',
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),

View File

@@ -136,6 +136,28 @@ const physicalCapA = clamp(
);
const canRunAtMinCurrent = physicalCapA >= params.currentMinA;
// =====================================================
// FENETRE D'ETABLISSEMENT DU RESEAU (31/08/2026)
// =====================================================
// Mesure du 31/08 : a la fermeture du contacteur, le reseau reprend d'un coup
// TOUTE la charge de la maison et le solaire bascule vers la batterie. Batterie
// preneuse, la production ne bouge pas ; batterie pleine, elle s'effondre (2663
// -> 559 W) faute de destination. Dans les deux cas l'import decroit ensuite
// jusqu'a la consigne en ~150 s (1985 -> 25 W, puis 2600 -> 52 W).
//
// Pendant cette phase, PV, import et courant batterie ne decrivent aucun regime
// permanent : la boucle de 30 s y prend cinq decisions, et la moyenne PV 10 min
// en garde la trace une dizaine de minutes de plus. Le systeme lit "plus de
// soleil", en conclut qu'il a besoin du reseau, et confirme ainsi sa propre
// premisse -- une boucle de rattrapage, comme les trois du 24/08.
//
// ASYMETRIE VOULUE : la fenetre ne s'ouvre qu'a l'ARRIVEE du reseau. A sa
// disparition, les onduleurs reprennent instantanement toute la charge : les
// gardes doivent agir plus vite, jamais moins vite.
const gridSettling =
parsed.gridAvailable &&
parsed.gridTransitionAgeSec < params.gridSettleSeconds;
const endDayGuardSoc = Math.max(params.endOfDaySocGuard, params.endOfDaySocTarget);
const energyNeededForEndDayGuardKwh = Math.max(
@@ -805,6 +827,8 @@ return [{
systemCapCurrentA,
physicalCapA,
canRunAtMinCurrent,
gridSettling,
gridTransitionAgeSec: parsed.gridTransitionAgeSec,
endDayGuardSoc,
energyNeededForEndDayGuardKwh,
forecastBufferKwh,