moteur : la marge de soutirage borne aussi les consignes d'un optimiseur (défauts 4 et 5)
La marge que le compteur laisse sous le seuil ne bornait que la cascade du rule-based. Sous un optimiseur qui demande le maximum, ce que la protection venait de retirer revenait au cycle suivant, dès que le compteur repassait sous la limite : 32,2 A pendant une minute sur l'installation d'essai, après un délestage du chauffe-eau. bornerOptimiseurAuSoutirage(), avant le dispatch : une consigne d'optimiseur ne fait monter une charge que de la marge libre (pire phase, consommée dans l'ordre du créneau). Elle ne réduit rien — c'est la protection qui réduit. L'engagé d'une borne est sa dernière commande tenue, si bien que le dispatch ne défait plus, dans le même cycle, ce que la protection vient de faire. Une consigne écrêtée publie DRAW_CAP et clamped, plus le motif de l'optimiseur (défaut 5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
440a45f526
commit
5ab5213aad
@ -17,6 +17,7 @@
|
||||
#include "scheduler/optimizerstatus.h"
|
||||
#include "scheduler/publishedplan.h"
|
||||
#include "scheduler/optimizertrace.h"
|
||||
#include "scheduler/optimizerplan.h"
|
||||
#include "types/decisionreason.h"
|
||||
#include "types/surpluscontext.h"
|
||||
#include "types/plan.h"
|
||||
@ -532,6 +533,12 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
||||
// adjustEvChargers() juste après, et c'est ce qui empêche le proxy de re-commander.
|
||||
m_waterfallEvIds = plan.waterfallEvIds;
|
||||
|
||||
// Lot délestage triphasé — la marge de soutirage s'oppose aussi aux consignes d'un OPTIMISEUR,
|
||||
// avant qu'elles partent : ce que la protection a retiré ne revient pas tant que le compteur
|
||||
// ne lui en laisse pas la place. Le rule-based, lui, est déjà borné dans sa cascade.
|
||||
if (plan.strategy == QLatin1String(OptimizerPlan::StrategieOptimiseur))
|
||||
bornerOptimiseurAuSoutirage(slot, ctx);
|
||||
|
||||
// 7 : dispatch matériel (même position que l'amont — m_chargingActions rempli par getPlan())
|
||||
applyActionsToAdapters(slot, currentDateTime); // charges arbitrées (Setpoint W, State) ET bornes du waterfall
|
||||
adjustEvChargers(currentDateTime); // bornes RESTÉES au proxy : échéance, spot market
|
||||
@ -979,6 +986,82 @@ void EnergyArbitrator::shedWaterfallForOverload(const QDateTime &now)
|
||||
"ce qui pouvait l'être. La cause est hors du waterfall.";
|
||||
}
|
||||
|
||||
void EnergyArbitrator::bornerOptimiseurAuSoutirage(Slot &slot, const SurplusContext &ctx)
|
||||
{
|
||||
const DrawMargin marge = ctx.drawMargin;
|
||||
if (!marge.borne)
|
||||
return; // aucun plafond en vigueur : rien à opposer
|
||||
|
||||
QHash<QString, const LoadContext *> charges;
|
||||
for (const LoadContext &lc : ctx.loads)
|
||||
charges.insert(lc.id, &lc);
|
||||
|
||||
double libreParPhaseW = qMax(0.0, marge.watts);
|
||||
for (LoadAction &a : slot.actions) {
|
||||
if (!OptimizerPlan::estActionOptimiseur(a) || m_shedActions.contains(a.loadId))
|
||||
continue;
|
||||
const LoadContext *lc = charges.value(a.loadId, nullptr);
|
||||
if (!lc || !lc->telemetry.available)
|
||||
continue;
|
||||
const bool borne = lc->adapter == QLatin1String("evcharger");
|
||||
const int phases = qMax(1, lc->declared.phases);
|
||||
|
||||
// Ce que la charge TIENT déjà, et qu'on ne lui retire pas ici.
|
||||
double engageW = qMax(0.0, lc->telemetry.currentPowerW);
|
||||
if (borne)
|
||||
if (const std::optional<double> commandee = puissanceCommandee(a.loadId))
|
||||
engageW = qMax(0.0, *commandee);
|
||||
|
||||
// Ce que l'action poserait, écrêtée au matériel comme l'adaptateur le fera. Pour une borne,
|
||||
// le matériel est son courant maximal sur ses phases : `declared.maxPowerW` est l'enveloppe
|
||||
// de la cascade, que l'EvAdapter ne réapplique pas.
|
||||
const double materielW = borne ? lc->declared.maxA * 230.0 * phases : lc->declared.maxPowerW;
|
||||
double demandeW = 0;
|
||||
if (a.kind == LoadAction::Setpoint)
|
||||
demandeW = materielW > 0 ? qMin(a.powerW, materielW) : a.powerW;
|
||||
else if (a.kind == LoadAction::State)
|
||||
demandeW = lc->declared.estimatedPowerW.value(a.state, 0.0);
|
||||
else
|
||||
continue;
|
||||
|
||||
const double plafondW = engageW + libreParPhaseW * phases;
|
||||
if (demandeW <= plafondW + 0.5) {
|
||||
libreParPhaseW = qMax(0.0, libreParPhaseW - qMax(0.0, demandeW - engageW) / phases);
|
||||
continue;
|
||||
}
|
||||
|
||||
double accordeW = plafondW;
|
||||
if (a.kind == LoadAction::Setpoint) {
|
||||
if (borne) // l'adaptateur arrondit au plus proche : on lui donne un ampère entier
|
||||
accordeW = std::floor(plafondW / (230.0 * phases) + 1e-6) * 230.0 * phases;
|
||||
a.powerW = accordeW;
|
||||
a.estimatedPowerW = accordeW;
|
||||
} else {
|
||||
// L'état le plus haut qui tient sous le plafond. Les états 1 et 2 valent 0 W : c'est 2
|
||||
// qui est retenu, jamais 1 (ECS-413).
|
||||
int etat = 2;
|
||||
for (int e = 3; e <= 4; ++e)
|
||||
if (lc->declared.estimatedPowerW.value(e, 0.0) <= plafondW + 0.5)
|
||||
etat = e;
|
||||
a.state = etat;
|
||||
accordeW = lc->declared.estimatedPowerW.value(etat, 0.0);
|
||||
}
|
||||
libreParPhaseW = qMax(0.0, libreParPhaseW - qMax(0.0, accordeW - engageW) / phases);
|
||||
|
||||
// DÉFAUT 5 — le motif est celui du mécanisme qui a produit l'issue, pas celui que
|
||||
// l'optimiseur avait donné à sa demande. Le niveau unique `optimizer` reste : c'est
|
||||
// l'optimiseur qui a décidé, la sécurité qui a borné.
|
||||
a.reason = { DecisionCode::DrawCap, drawCapReasonParams(marge, false, 0, false) };
|
||||
a.clamped = true;
|
||||
if (!a.levels.isEmpty())
|
||||
a.levels.first().reason = a.reason;
|
||||
qCInfo(dcNymeaEnergy()).noquote()
|
||||
<< "[Arbitre] Garde de soutirage" << a.loadId << "— l'optimiseur demandait"
|
||||
<< qRound(demandeW) << "W, la charge tient" << qRound(engageW) << "W, accordé"
|
||||
<< qRound(accordeW) << "W |" << renderFr(a.reason, lc->label);
|
||||
}
|
||||
}
|
||||
|
||||
void EnergyArbitrator::fusionnerDelestageDansSlot(Slot &slot) const
|
||||
{
|
||||
for (auto it = m_shedActions.constBegin(); it != m_shedActions.constEnd(); ++it) {
|
||||
|
||||
@ -803,6 +803,39 @@ public:
|
||||
*/
|
||||
void shedWaterfallForOverload(const QDateTime &now);
|
||||
|
||||
/*!
|
||||
* \brief Garde de soutirage : une consigne d'OPTIMISEUR ne fait monter une charge que de la
|
||||
* marge que le compteur laisse sous le seuil (lot délestage triphasé, défauts 4 et 5).
|
||||
*
|
||||
* \param slot Créneau du cycle, modifié en place AVANT le dispatch. Seules les actions
|
||||
* d'optimiseur (niveau unique \c optimizer) sont lues ; celles que L4 revendique ce cycle
|
||||
* (\c m_shedActions) sont laissées au délestage.
|
||||
* \param ctx Contexte du cycle : \c drawMargin (seuil, phase la plus chargée, signée) et
|
||||
* la télémétrie des charges.
|
||||
*
|
||||
* \par Pourquoi
|
||||
* La marge de soutirage bornait la cascade du rule-based, et RIEN d'autre : un optimiseur qui
|
||||
* demande le maximum voyait ses consignes écrêtées au matériel seulement. Relevé le 2026-10-27 :
|
||||
* la protection déleste, le compteur repasse juste sous la limite, le délestage se relâche,
|
||||
* l'optimiseur redemande 3 kW, et la surcharge reprend une minute. La règle de sécurité était
|
||||
* celle du disjoncteur ; elle devient celle du moteur, quel que soit le décideur.
|
||||
*
|
||||
* \par Contrat
|
||||
* - Seule une HAUSSE est bornée, jamais une baisse : réduire est le geste de la protection, et
|
||||
* la garde ne réduit rien sous ce que la charge tient déjà. Sous un compteur en dépassement
|
||||
* (marge négative), la charge garde ce qu'elle tient.
|
||||
* - Plafond = engagé + marge libre × phases de la charge. L'engagé d'une borne est sa dernière
|
||||
* commande TENUE (la protection l'a peut-être rabaissée dans ce même cycle) ; sinon sa mesure.
|
||||
* Celui des autres charges est leur télémétrie.
|
||||
* - La marge est la pire phase, et chaque hausse servie la consomme pour les suivantes, dans
|
||||
* l'ordre du créneau.
|
||||
* - Une consigne écrêtée porte \c DRAW_CAP (le mécanisme qui a produit l'issue, règle 7-b) et
|
||||
* \c clamped ; une borne est écrêtée à l'ampère entier inférieur, une PAC à l'état le plus haut
|
||||
* qui tient (jamais 1).
|
||||
* - Sans plafond en vigueur (limite non réglée), rien n'est borné.
|
||||
*/
|
||||
void bornerOptimiseurAuSoutirage(Slot &slot, const SurplusContext &ctx);
|
||||
|
||||
//! \brief Replace dans \c slot les décisions des charges délestées par L4.
|
||||
//! \param[in,out] slot Actions du cycle. Une charge délestée porte le motif du DÉLESTAGE :
|
||||
//! le plan ne l'a pas commandée, publier sa consigne montrerait ce qui n'a pas eu lieu.
|
||||
|
||||
@ -555,7 +555,9 @@ QList<Ecretage> finaliser(Plan &plan, Slot &slot, const QHash<QString, LoadActio
|
||||
a.counts = niveau.counts;
|
||||
a.levels = {niveau};
|
||||
|
||||
a.clamped = !applique || differe(demande, appliquees.value(a.loadId), adapter);
|
||||
// Déjà écrêtée par la garde de soutirage de l'arbitre, avant le dispatch : la demande
|
||||
// reçue ici est la demande bornée, l'écrêtage reste un écrêtage.
|
||||
a.clamped = demande.clamped || !applique || differe(demande, appliquees.value(a.loadId), adapter);
|
||||
if (a.clamped)
|
||||
ecretages.append({a.loadId, decrire(demande, adapter),
|
||||
applique ? decrire(a, adapter) : QStringLiteral("non appliquée")});
|
||||
|
||||
@ -1030,8 +1030,9 @@ void TestRefOptimizer::testTheDrawCapHoldsAgainstAnOptimizerAtMaximum()
|
||||
QCOMPARE(borneA(), 16);
|
||||
QCOMPARE(var2W(), 3000.0);
|
||||
|
||||
// 5. LA BORNE. 32 A par phase : la protection la rabaisse sans la couper.
|
||||
pas(32 * 230 * 3, 32);
|
||||
// 5. LA BORNE. 26 A par phase : la protection la rabaisse sans la couper. (Le harnais rappelle
|
||||
// la protection après le cycle sur le MÊME compteur : au-delà, deux gestes la coupent.)
|
||||
pas(26 * 230 * 3, 26);
|
||||
const int borneApres = borneA();
|
||||
QVERIFY2(borneApres > 0 && borneApres < 16, qUtf8Printable(QString::number(borneApres)));
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user