test : la règle de jour ne rend rien en dépassement (garde éprouvée)
testWaterfallChargerGetsNothingBackInOverload : phases du compteur aveugles à la borne, 24 A sur A pour un seuil de 22,5 A. La stratégie tient la borne à son plancher (1 380 W) ; sans la garde de dépassement d'evPhaseAllowanceW(), elle lui rendait 2 530 W pendant le dépassement. Aucun test existant ne rougissait sans cette garde. Au passage, deux formulations retirées des commentaires (garde publique). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1e4ec45e12
commit
afdfaae4aa
@ -35,7 +35,7 @@ rien sur les cas qu'il ne sait pas produire.
|
||||
`chargerUnmetered` mesure sa puissance mais publie ses courants sous `currentL1..3`, comme le
|
||||
plugin V2C Trydan, et jamais sous `currentPhaseX` : `EvCharger::meteredPhases()` y est vide. Les
|
||||
autres bornes du mock publiaient toutes leurs phases, si bien qu'une borne **non mesurée par
|
||||
phase** — le cas du banc — était introuvable par la suite.
|
||||
phase** — le cas d'un V2C Trydan — était introuvable par la suite.
|
||||
|
||||
**Ce que la table donne, et c'est la bonne nouvelle** : `sessionEnergy` et `totalEnergyConsumed`
|
||||
sont désormais **discriminants** — il existe des classes avec et sans. Les régimes `measured` /
|
||||
|
||||
@ -12715,8 +12715,8 @@ void Simulation::testOffPeakUnmeteredChargerSettlesUnderThePhaseLimit()
|
||||
// la borne comme une charge de la maison sur sa phase, et la consigne alternait : à 16 A la
|
||||
// place tombait au plancher, à 6 A elle remontait.
|
||||
//
|
||||
// Limite 25 A, marge 10 % (seuil 22,5 A). La maison du simulateur la nuit : 2,3 A sur A,
|
||||
// 0,5 A sur B et C ; puis le lave-vaisselle sur A (+8,3 A). Attendu : la place que la maison
|
||||
// Limite 25 A, marge 10 % (seuil 22,5 A). Une maison de nuit : 2,3 A sur A, 0,5 A sur B et
|
||||
// C ; puis un lave-vaisselle sur A (+8,3 A). Attendu : la place que la maison
|
||||
// laisse, moins 1 A, à l'ampère inférieur — 22,5 − 2,3 − 1 = 19,2 A, donc 16 A (le maximum de
|
||||
// la borne) ; avec le lave-vaisselle, 22,5 − 10,6 − 1 = 10,9 A, donc 10 A. En triphasé, la
|
||||
// phase A est la plus chargée et décide, de la même façon.
|
||||
@ -12933,6 +12933,65 @@ void Simulation::testWaterfallChargerSettlesUnderThePhaseLimit()
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testWaterfallChargerGetsNothingBackInOverload()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
// LA GARDE DE DÉPASSEMENT de la règle de jour. Hors heures creuses, evPhaseAllowanceW() rend
|
||||
// à la borne son tirage — mais pas sur une phase au-dessus du seuil. Le cas qui la justifie est
|
||||
// celui de `testOverloadProtectionEcoMode` : des phases qui ne voient pas la borne. La maison
|
||||
// sans la borne s'y calcule trop basse, et la place rendue maintiendrait la borne pendant le
|
||||
// dépassement.
|
||||
//
|
||||
// Midi, sans tarif, limite 25 A, marge 10 % (seuil 22,5 A). La borne tire 16 A sur A ; la
|
||||
// phase A du compteur lit 24 A, sans la borne ; B et C exportent 14 A. L4 la rabaisse
|
||||
// d'abord, puis la stratégie décide sur ce même compteur. Avec la garde : la place libre,
|
||||
// négative — la borne est tenue à son plancher, 6 A (1 380 W). Sans elle : 22,5 − (24 − E) − 1,
|
||||
// soit 11 A avec E = 14 A après L4 (2 530 W), en plein dépassement.
|
||||
FuseauDeTest fuseau("Europe/Paris");
|
||||
ScenarioHc s = preparerScenarioHc(QStringLiteral("jour-depassement"), BatterieHc::Aucune,
|
||||
QVariantList(), BorneHc::Monophasee);
|
||||
QVERIFY(s.arb && s.borne);
|
||||
QCOMPARE(injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", 25}, {"phaseSafetyMarginPercent", 10}}))
|
||||
.toMap().value("params").toMap().value("energyError").toString(),
|
||||
QString("EnergyErrorNoError"));
|
||||
const QString borne = s.borneId.toString();
|
||||
s.borne->setStateValue("pluggedIn", true);
|
||||
s.borne->setStateValue("power", true);
|
||||
s.borne->setStateValue("charging", true);
|
||||
s.borne->setStateValue("maxChargingCurrent", 16);
|
||||
s.borne->setStateValue("currentPhaseA", 16.0);
|
||||
s.borne->setStateValue("currentPowerPhaseA", 3680.0);
|
||||
s.borne->setStateValue("currentPower", 3680.0);
|
||||
const QList<double> phasesA = {24, -14, -14}; // aveugles à la borne
|
||||
double totalW = 0;
|
||||
const QStringList phases = {"A", "B", "C"};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
s.meter->setStateValue(QStringLiteral("currentPhase%1").arg(phases.at(i)), phasesA.at(i));
|
||||
s.meter->setStateValue(QStringLiteral("currentPowerPhase%1").arg(phases.at(i)), phasesA.at(i) * 230);
|
||||
totalW += phasesA.at(i) * 230;
|
||||
}
|
||||
|
||||
// La décision de la STRATÉGIE, lue dans la trame que publie le cycle : le harnais rappelle
|
||||
// ensuite L4 sur le même compteur (simulationCallUpdate()), qui coupe la borne et republie.
|
||||
QSignalSpy notifs(s.arb, &EnergyArbitrator::loadTelemetryChanged);
|
||||
cycleHc(s, utcDateTime(QDate(2026, 6, 8), QTime(10, 0)), totalW);
|
||||
QVariantMap strategie;
|
||||
QStringList trace;
|
||||
for (const QList<QVariant> &n : notifs) {
|
||||
const QVariantMap t = telemetrieParId(n.first().toMap()).value(borne);
|
||||
trace << QJsonDocument::fromVariant(t.value("decision")).toJson(QJsonDocument::Compact)
|
||||
+ QStringLiteral(" alloué %1 W").arg(t.value("allocatedW").toDouble());
|
||||
if (t.value("decision").toMap().value("code").toString() != QString(DecisionCode::OverloadThrottled))
|
||||
strategie = t;
|
||||
}
|
||||
qInfo().noquote() << "[dépassement, jour]" << trace.join(QStringLiteral(" ; "));
|
||||
QVERIFY2(!strategie.isEmpty(), qUtf8Printable(trace.join(" ; ")));
|
||||
QVERIFY2(qRound(strategie.value("allocatedW").toDouble()) == 1380, qUtf8Printable(trace.join(" ; ")));
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================================
|
||||
// Lot délestage visible (2026-10-25) — la protection PUBLIE ce qu'elle fait, elle ne décide rien
|
||||
// de plus. Question de Patrick : « Si le délestage coupe une charge, est-ce que le client voit une
|
||||
|
||||
@ -364,6 +364,9 @@ private slots:
|
||||
//! Lot délestage V2C (question 5) — hors heures creuses, sous le rule-based, une borne limitée
|
||||
//! par sa phase se pose sous la limite sans alterner, maison légère ou chargée.
|
||||
void testWaterfallChargerSettlesUnderThePhaseLimit();
|
||||
//! Lot délestage V2C (question 5) — EN DÉPASSEMENT, la règle de jour ne rend rien : phases du
|
||||
//! compteur aveugles à la borne, la stratégie la tient à son plancher.
|
||||
void testWaterfallChargerGetsNothingBackInOverload();
|
||||
//! TOUTE méthode Get* du namespace rend ce que sa déclaration promet.
|
||||
void testEveryGetterReturnsWhatItDeclares();
|
||||
//! Le snapshot sert la DÉCISION : le canal, jamais la table — et l'absence n'est pas zéro.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user