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
|
`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
|
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
|
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`
|
**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` /
|
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
|
// 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.
|
// 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,
|
// Limite 25 A, marge 10 % (seuil 22,5 A). Une maison de nuit : 2,3 A sur A, 0,5 A sur B et
|
||||||
// 0,5 A sur B et C ; puis le lave-vaisselle sur A (+8,3 A). Attendu : la place que la maison
|
// 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
|
// 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
|
// 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.
|
// phase A est la plus chargée et décide, de la même façon.
|
||||||
@ -12933,6 +12933,65 @@ void Simulation::testWaterfallChargerSettlesUnderThePhaseLimit()
|
|||||||
#endif
|
#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
|
// 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
|
// 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
|
//! 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.
|
//! par sa phase se pose sous la limite sans alterner, maison légère ou chargée.
|
||||||
void testWaterfallChargerSettlesUnderThePhaseLimit();
|
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.
|
//! TOUTE méthode Get* du namespace rend ce que sa déclaration promet.
|
||||||
void testEveryGetterReturnsWhatItDeclares();
|
void testEveryGetterReturnsWhatItDeclares();
|
||||||
//! Le snapshot sert la DÉCISION : le canal, jamais la table — et l'absence n'est pas zéro.
|
//! 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