diff --git a/docs/DESIGN_3g.md b/docs/DESIGN_3g.md index 9359929..65d5249 100644 --- a/docs/DESIGN_3g.md +++ b/docs/DESIGN_3g.md @@ -117,6 +117,18 @@ et **aucune n'est neutre** : > changelog comme un changement de comportement assumé. Une batterie sous son seuil de réserve > ne devrait pas plus alimenter un chauffe-eau qu'une voiture. +**DÉCISION : montée dans le budget**, appliquée en 3g-1. + +**Complément 3g-2, trouvé en exécutant vraiment les décisions du waterfall.** La règle ne +mordait que sur les AUGMENTATIONS : le recrédit anti-clignotement rendait à chaque charge sa +propre consommation, y compris sous la réserve, si bien qu'une charge déjà en marche le restait. +Le motif publié disait pourtant « aucune charge n'est servie ». **Sous la réserve, le recrédit +est désormais supprimé** — le budget est une décision de retenir, pas une mesure, et recréditer +dedans rend exactement ce qu'on venait de confisquer (LM-1203-b : « annulé, pas réduit »). +Le motif `BATTERY_RESERVE` chiffre en conséquence les deux termes retenus : le surplus annulé +**et** la consommation propre de la charge. Éprouvé par `batteryLevelConsiderationStopCharging`, +qui échouait avant cette correction. + ### 3.3 Le nombre de phases se décide en ampères, le waterfall parle en watts `getBestPhaseCount()` choisit 1 ou 3 phases **à partir du courant brut**, puis divise. Le diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.cpp b/energyplugin/etm/scheduler/rulebasedscheduler.cpp index 5f64388..a3fa8e4 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.cpp +++ b/energyplugin/etm/scheduler/rulebasedscheduler.cpp @@ -236,8 +236,17 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc, // Recrédit anti-clignotement (Correction B / contrat rév. 2 §5) : la conso de DÉBUT de cycle // est déjà soustraite de l'export mesuré → on la rend au budget local avant d'arrondir. // PAS de relecture post-setpoint (invariant 8 : aucune boucle de feedback). - const double budgetW = remainingSurplusW + lc.telemetry.currentPowerW; - budget.recreditedW += lc.telemetry.currentPowerW; + // 3g-2 — SAUF sous la réserve de stockage, et c'est la seule exception. Le recrédit + // suppose que le budget est une MESURE dont la conso de la charge a déjà été retranchée ; + // sous la réserve, le budget n'est pas une mesure mais une DÉCISION de retenir + // (LM-1203-b : « le budget est annulé, pas réduit »). Recréditer dans un budget décidé nul + // rend exactement ce qu'on venait de confisquer : toute charge déjà en marche le restait, + // la réserve ne mordait que sur les augmentations, et le motif publié — « aucune charge + // n'est servie » — était faux. Attrapé par batteryLevelConsiderationStopCharging au moment + // où les bornes ont commencé à exécuter réellement les décisions du waterfall. + const double recreditW = m_reserveActive ? 0.0 : lc.telemetry.currentPowerW; + const double budgetW = remainingSurplusW + recreditW; + budget.recreditedW += recreditW; const QList &levels = lc.declared.powerLevels; const bool dynamic = levels.isEmpty(); @@ -384,14 +393,20 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc, la.reason = { DecisionCode::SurplusSetpoint, {{"budgetW", qRound(budgetW)}, {"setpointW", qRound(setpointW)}, {"stepped", !dynamic}} }; - else if (m_reserveActive && m_reserveWithheldW > 0) + else if (m_reserveActive && (m_reserveWithheldW + lc.telemetry.currentPowerW) > 0) // 3g-1 — il Y A du surplus, et une règle l'interdit. À ne pas confondre avec // « il n'y en a pas » : le geste attendu n'est pas le même, et le second envoie // chercher une panne inexistante. + // + // 3g-2 — ce que la réserve retient à CETTE charge a deux termes, et il faut les deux : + // le surplus annulé (le même pour toutes), ET sa propre consommation, que le recrédit + // lui aurait rendue. Sans le second, une charge en marche coupée par la réserve sur un + // surplus nul publiait « surplus insuffisant » — vrai sur le surplus, faux sur la + // cause : sans la réserve, elle aurait continué de tourner. la.reason = { DecisionCode::BatteryReserve, {{"socPercent", qRound(m_reserveSocPct)}, {"reservePercent", qRound(m_reserveSeuilPct)}, - {"withheldW", qRound(m_reserveWithheldW)}} }; + {"withheldW", qRound(m_reserveWithheldW + lc.telemetry.currentPowerW)}} }; else if (sousPlancher) // §12 — dire QUE le budget est sous le plancher, pas qu'il est insuffisant : la // seconde phrase enverrait chercher du soleil là où il faut lire une fiche technique. @@ -416,7 +431,12 @@ LoadAction RuleBasedScheduler::buildSgReadyStateAction(const LoadContext &lc, // Recrédit (correction B) : la puissance ALLOUÉE de l'état courant (déclaré, 0 pour 1/2) // revient au budget — comme l'ECS. Base : "quel surplus si la PAC n'était pas pilotée ?" - const double allocatedNowW = lc.declared.estimatedPowerW.value(currentState, 0.0); + // 3g-2 — même exception que pour les charges en watts : sous la réserve, pas de recrédit. + // Une PAC en état 3 ou 4 y resterait sinon indéfiniment, la réserve n'agissant que sur les + // montées d'état. Voir la note détaillée dans buildSetpointAction(). + const double allocatedNowW = m_reserveActive + ? 0.0 + : lc.declared.estimatedPowerW.value(currentState, 0.0); const double budgetW = remainingSurplusW + allocatedNowW; budget.recreditedW += allocatedNowW;