test : un vrai refus reste vu, sur la borne et sur les autres charges
testARealRefusalIsStillSeen — borne figée sous une consigne à la hausse : refus au cycle qui suit la commande, comme avant le correctif ; montée qui progresse puis cale : latence tant qu'elle se rapproche, refus au cycle où elle cale ; descente suivie de 500 W seulement : refus au cycle suivant, la progression n'excuse jamais une descente. testWattLoadsFollowedOrRefusedUnderAMeter — relais et consigne continue sous compteur dédié : marche suivie dans les deux sens sans refus ; refus réel vu au deuxième cycle (la latence par défaut de 60 s n'est pas dépassée par un cycle de 60 s) ; sans compteur, rien ne change. testSgReadyDivergenceIsPublishedOnly — sous compteur dédié, une PAC qui monte en minutes n'est pas divergente, une PAC qui ne démarre pas l'est ; son recrédit (puissance déclarée) n'en dépend pas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c4e517a8d9
commit
e856c48084
@ -9324,6 +9324,375 @@ void Simulation::testRecreditSuspendedAfterAnObeyedStep()
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Un VRAI refus reste vu, au cycle qui suit la commande — et la descente n'a pas d'excuse.
|
||||
*
|
||||
* Même banc que testRecreditSuspendedAfterAnObeyedStep(). Trois bornes qui n'obéissent pas :
|
||||
* - **montée refusée** : la borne tient 7 A, la consigne passe à 13 A, le véhicule reste à 7 A —
|
||||
* mesure figée. Refus au cycle qui suit la commande, c'est-à-dire au même cycle qu'avant le
|
||||
* correctif ;
|
||||
* - **montée qui progresse puis cale** : rampe vers 13 A, calée à 2 000 W. Tant qu'elle se
|
||||
* rapproche, c'est une latence ; le cycle où elle cale, c'est un refus ;
|
||||
* - **descente non suivie** : la production s'effondre, la consigne descend, la borne ne lâche
|
||||
* que 500 W. Elle se « rapproche » de sa consigne, et c'est pourtant un refus, au cycle qui
|
||||
* suit la commande de descente : la progression n'excuse jamais une descente.
|
||||
*
|
||||
* \note Les deux marches dont on lit le MOTIF tombent après les 300 s du verrou de marche/arrêt
|
||||
* armé au démarrage. Avant, l'arrêt verrouillé engage le courant minimal (P2) et le motif
|
||||
* publié est \c LOCK_MIN_ON, qui précède le refus ; le verdict, lui, est le même.
|
||||
*/
|
||||
void Simulation::testARealRefusalIsStillSeen()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
// ── 1. Montée refusée : 7 A suivis jusqu'à k=5, consigne de 13 A à k=5, figée à 7 A. ──
|
||||
// La production retombe à k=6 : sans recrédit, le budget ne paie plus le plancher, et le
|
||||
// motif qui l'explique est la suspension.
|
||||
const QList<CycleBorne> figee = rejouerBorne("refus-montee",
|
||||
[](int k, double commandeW, double tireW) { return k <= 5 ? commandeW : tireW; },
|
||||
[](int k) { return k < 5 ? 1910.0 : (k == 5 ? 3300.0 : 2000.0); }, 7);
|
||||
// ── 2. Montée qui progresse (la moitié de l'écart par cycle) puis cale à 2 000 W. ──────
|
||||
const QList<CycleBorne> cale = rejouerBorne("refus-cale",
|
||||
[](int, double commandeW, double tireW) {
|
||||
const double rampe = commandeW > tireW ? tireW + (commandeW - tireW) * 0.5 : commandeW;
|
||||
return qMin(rampe, 2000.0);
|
||||
}, [](int) { return 3300.0; }, 5);
|
||||
// ── 3. Descente non suivie : production 3 300 → 1 900 W à k=6, la borne ne lâche que 500 W.
|
||||
const QList<CycleBorne> descente = rejouerBorne("refus-descente",
|
||||
[](int k, double commandeW, double tireW) {
|
||||
if (k <= 6)
|
||||
return commandeW; // elle a obéi jusqu'à la commande de descente
|
||||
return commandeW < tireW ? qMax(commandeW, tireW - 500.0) : commandeW;
|
||||
}, [](int k) { return k >= 6 ? 1900.0 : 3300.0; }, 8);
|
||||
|
||||
QCOMPARE(figee.size(), 7);
|
||||
QCOMPARE(cale.size(), 5);
|
||||
QCOMPARE(descente.size(), 8);
|
||||
|
||||
// 1. 7 A suivis : aucune suspension. Commande de 13 A à k=5 ; à k=6 la borne tire toujours
|
||||
// 1 610 W : refus AU CYCLE SUIVANT. Rien ne s'est rapproché, aucune latence ne l'excuse.
|
||||
for (int k = 1; k <= 4; ++k) {
|
||||
QCOMPARE(figee.at(k).consigneA, 7);
|
||||
QVERIFY(figee.at(k).motif != QLatin1String(DecisionCode::RecreditSuspended));
|
||||
}
|
||||
QCOMPARE(figee.at(5).consigneA, 13);
|
||||
QCOMPARE(figee.at(6).mesureW, 1610);
|
||||
QCOMPARE(figee.at(6).motif, QString(DecisionCode::RecreditSuspended));
|
||||
QCOMPARE(figee.at(6).retenuW, 1610);
|
||||
QCOMPARE(figee.at(6).recrediteW, 0);
|
||||
QVERIFY(figee.at(6).divergent);
|
||||
// Daté à l'instant de la commande (k=5), en temps de cycle : un cycle, 60 s.
|
||||
QCOMPARE(figee.at(6).refusDepuisS, 60);
|
||||
|
||||
// 2. 0 → 1 495 W à k=1 (rapprochée de 1 495 W) : latence, recrédit rendu, pas divergent.
|
||||
// 1 495 → 2 000 W à k=2 (rapprochée de 505 W) : latence encore. 2 000 W à k=3 : calée, refus.
|
||||
QCOMPARE(cale.at(0).consigneA, 13);
|
||||
QCOMPARE(cale.at(1).mesureW, 1495);
|
||||
QCOMPARE(cale.at(1).recrediteW, 1495);
|
||||
QVERIFY(!cale.at(1).divergent);
|
||||
QCOMPARE(cale.at(2).mesureW, 2000);
|
||||
QCOMPARE(cale.at(2).recrediteW, 2000);
|
||||
QVERIFY(!cale.at(2).divergent);
|
||||
QCOMPARE(cale.at(3).mesureW, 2000);
|
||||
QCOMPARE(cale.at(3).recrediteW, 0);
|
||||
QVERIFY(cale.at(3).divergent);
|
||||
|
||||
// 3. Jusqu'à k=5, 13 A suivis. À k=6 la production tombe : la consigne descend. À k=7 la
|
||||
// borne n'a lâché que 500 W — elle s'est rapprochée de sa consigne, et c'est un refus,
|
||||
// AU CYCLE QUI SUIT la commande de descente : jugé sur la latence de descente (7 s), sans
|
||||
// l'excuse de la progression.
|
||||
QCOMPARE(descente.at(5).consigneA, 13);
|
||||
QVERIFY2(descente.at(6).consigneA < 13, "la production tombe : la consigne doit descendre");
|
||||
QCOMPARE(descente.at(7).mesureW, 2490);
|
||||
QCOMPARE(descente.at(7).motif, QString(DecisionCode::RecreditSuspended));
|
||||
QCOMPARE(descente.at(7).retenuW, 2490);
|
||||
QCOMPARE(descente.at(7).recrediteW, 0);
|
||||
QVERIFY(descente.at(7).divergent);
|
||||
QCOMPARE(descente.at(7).refusDepuisS, 60);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Relais et consignes continues sous compteur dédié : marche suivie, refus réel, et sans
|
||||
* compteur rien ne change.
|
||||
*
|
||||
* Même défaut que la borne avant le correctif : l'écart comparait la consigne qui venait de
|
||||
* partir à la mesure de la précédente, et le verdict dépendait de la phase du battement de
|
||||
* publication. Jugé au début du cycle, il ne dépend plus que de la charge :
|
||||
* - **marche suivie** (le compteur voit la consigne au cycle suivant) : jamais de refus, dans
|
||||
* les deux sens ;
|
||||
* - **refus réel** (le compteur reste à 0) : latence au cycle qui suit la commande — le défaut
|
||||
* de 60 s n'est pas dépassé par un cycle de 60 s — puis refus au suivant ;
|
||||
* - **sans compteur** : relais sec, rien ne mesure, l'écart est absent ; consigne continue, la
|
||||
* mesure est celle de l'appareil, qui est aussi sa commande — l'écart est publié, jamais
|
||||
* divergent. Exactement comme avant.
|
||||
*
|
||||
* \note Pour une consigne continue, la « commande » est la puissance que l'appareil annonce
|
||||
* appliquer (\c puissanceCommandee()). Le modèle le fait annoncer la consigne qu'il a reçue.
|
||||
*/
|
||||
void Simulation::testWattLoadsFollowedOrRefusedUnderAMeter()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
constexpr double MaisonW = 300;
|
||||
struct Pas {
|
||||
int consigneW = 0;
|
||||
int recrediteW = 0;
|
||||
QString motif;
|
||||
int refusDepuisS = -1;
|
||||
int retenuW = 0;
|
||||
bool commande = false; //!< objet `command` publié
|
||||
bool divergent = false;
|
||||
};
|
||||
// mecanisme : "relais" (relais sec 1 000 W) ou "variable" (consigne continue 0-2 000 W).
|
||||
auto rejouer = [&](const QString &mecanisme, bool avecCompteur, bool obeit,
|
||||
const std::function<double(int)> &productionW, int cycles) -> QList<Pas> {
|
||||
QList<Pas> trace;
|
||||
const QString nom = mecanisme + (avecCompteur ? "-compteur" : "-seul") + (obeit ? "-suivie" : "-refus");
|
||||
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-" + nom + ".json";
|
||||
QFile::remove(cfgPath);
|
||||
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||||
const QUuid meterId = addMeter(27320);
|
||||
m_experiencePlugin->energyManager()->setRootMeter(meterId);
|
||||
const QUuid compteurId = addMeter(27321);
|
||||
Thing *meter = tm->findConfiguredThing(meterId);
|
||||
Thing *compteur = tm->findConfiguredThing(compteurId);
|
||||
if (!arb || !meter || !compteur)
|
||||
return trace;
|
||||
meter->setStateValue("connected", true);
|
||||
compteur->setStateValue("connected", true);
|
||||
compteur->setStateValue("currentPower", 0);
|
||||
|
||||
QString loadId;
|
||||
Thing *pilote = nullptr;
|
||||
QVariantMap cfg;
|
||||
if (mecanisme == QLatin1String("relais")) {
|
||||
const QUuid r = addDryRelay(27322);
|
||||
loadId = "ecs-relais";
|
||||
cfg = {{"id", loadId}, {"label", "ECS"}, {"adapter", "relay-router"}, {"mode", "fixed"},
|
||||
{"priority", 1}, {"enabled", true}, {"minOnS", 0}, {"minOffS", 0},
|
||||
{"relays", QVariantList() << QVariantMap{{"thingId", r.toString()}, {"powerW", 1000}}}};
|
||||
} else {
|
||||
const QUuid v = addEtmVariableLoad(27323);
|
||||
pilote = tm->findConfiguredThing(v);
|
||||
if (!pilote)
|
||||
return trace;
|
||||
pilote->setStateValue("currentPowerW", 0);
|
||||
loadId = v.toString();
|
||||
cfg = {{"id", loadId}, {"label", "Routeur PV"}, {"adapter", "etmvariableload"},
|
||||
{"mode", "dynamic"}, {"maxPowerW", 2000}, {"priority", 1}, {"enabled", true}};
|
||||
}
|
||||
if (avecCompteur)
|
||||
cfg.insert("meterThingId", compteurId.toString());
|
||||
LoadConfigStore *store = new LoadConfigStore(arb);
|
||||
arb->setLoadConfigStore(store);
|
||||
LoadConfigs cfgs;
|
||||
cfgs.append(LoadConfig::fromMap(cfg));
|
||||
QString err;
|
||||
if (!store->setConfigs(cfgs, &err))
|
||||
return trace;
|
||||
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
double commandeW = 0;
|
||||
for (int k = 0; k < cycles; ++k) {
|
||||
// Suivie : ce qu'on a commandé au cycle précédent coule. Refusée : rien ne coule.
|
||||
const double tireW = obeit ? commandeW : 0.0;
|
||||
compteur->setStateValue("currentPower", tireW);
|
||||
if (pilote)
|
||||
pilote->setStateValue("currentPowerW", commandeW); // l'appareil annonce sa consigne
|
||||
meter->setStateValue("currentPower", MaisonW + tireW - productionW(k));
|
||||
arb->simulationCallUpdate(t0.addSecs(60 * k));
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
const QVariantMap snap = arb->loadTelemetry();
|
||||
const QVariantMap e = telemetrieParId(snap).value(loadId);
|
||||
const QVariantMap mech = e.value("mechanism").toMap();
|
||||
commandeW = mech.value(mecanisme == QLatin1String("relais") ? "stageW" : "setpointW").toDouble();
|
||||
const QVariantMap params = e.value("decision").toMap().value("params").toMap();
|
||||
Pas p;
|
||||
p.consigneW = qRound(commandeW);
|
||||
p.recrediteW = qRound(snap.value("budget").toMap().value("recreditedW").toDouble());
|
||||
p.motif = e.value("decision").toMap().value("code").toString();
|
||||
p.refusDepuisS = params.value("refusedSinceS", -1).toInt();
|
||||
p.retenuW = params.value("withheldW").toInt();
|
||||
p.commande = e.contains("command");
|
||||
p.divergent = e.value("command").toMap().value("divergent").toBool();
|
||||
trace.append(p);
|
||||
qInfo().noquote() << QStringLiteral("[%1] k=%2 consigne %3 W | recrédit %4 | %5 retenu %6 | divergent %7")
|
||||
.arg(nom).arg(k).arg(p.consigneW).arg(p.recrediteW).arg(p.motif)
|
||||
.arg(p.retenuW).arg(p.divergent ? "oui" : "non");
|
||||
}
|
||||
qunsetenv("NYMEA_ENERGY_LOAD_CONFIG");
|
||||
QFile::remove(cfgPath);
|
||||
return trace;
|
||||
};
|
||||
|
||||
// Marche suivie : montée à k=0, descente à k=3, remontée à k=5.
|
||||
auto paliers = [](int k) { return (k >= 3 && k < 5) ? 1000.0 : 1800.0; };
|
||||
// Refus réel : de quoi commander au début, plus rien ensuite — le budget ne tient alors que
|
||||
// par le recrédit, et sa suspension se lit dans le motif.
|
||||
auto relaisRefus = [](int k) { return k < 2 ? 1800.0 : 1000.0; };
|
||||
auto variableRefus = [](int k) { return k < 2 ? 2300.0 : 300.0; };
|
||||
|
||||
const QList<Pas> r1 = rejouer("relais", true, true, paliers, 7);
|
||||
const QList<Pas> r2 = rejouer("relais", true, false, relaisRefus, 4);
|
||||
const QList<Pas> r3 = rejouer("relais", false, false, relaisRefus, 4);
|
||||
const QList<Pas> v1 = rejouer("variable", true, true, paliers, 7);
|
||||
const QList<Pas> v2 = rejouer("variable", true, false, variableRefus, 4);
|
||||
const QList<Pas> v3 = rejouer("variable", false, false, variableRefus, 4);
|
||||
QCOMPARE(r1.size(), 7); QCOMPARE(r2.size(), 4); QCOMPARE(r3.size(), 4);
|
||||
QCOMPARE(v1.size(), 7); QCOMPARE(v2.size(), 4); QCOMPARE(v3.size(), 4);
|
||||
|
||||
// ── Marche suivie : elle a bien eu lieu, dans les deux sens, et jamais de refus. ─────────
|
||||
QCOMPARE(r1.at(0).consigneW, 1000);
|
||||
QCOMPARE(r1.at(3).consigneW, 0);
|
||||
QCOMPARE(r1.at(5).consigneW, 1000);
|
||||
QVERIFY(v1.at(3).consigneW < v1.at(2).consigneW);
|
||||
QVERIFY(v1.at(5).consigneW > v1.at(4).consigneW);
|
||||
for (const QList<Pas> *t : {&r1, &v1}) {
|
||||
for (int k = 1; k < t->size(); ++k) {
|
||||
QVERIFY2(t->at(k).motif != QLatin1String(DecisionCode::RecreditSuspended),
|
||||
qUtf8Printable(QStringLiteral("k=%1 : recrédit suspendu sur une charge qui obéit").arg(k)));
|
||||
QVERIFY(t->at(k).commande);
|
||||
QVERIFY2(!t->at(k).divergent, qUtf8Printable(QStringLiteral("k=%1 : divergent").arg(k)));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Refus réel : latence au cycle qui suit la commande (60 s ne dépasse pas le défaut de
|
||||
// 60 s), refus au suivant — recrédit suspendu, et le motif le dit. ─────────────────────
|
||||
for (const QList<Pas> *t : {&r2, &v2}) {
|
||||
QVERIFY(t->at(0).consigneW > 0);
|
||||
QVERIFY(t->at(1).recrediteW > 0);
|
||||
QVERIFY(!t->at(1).divergent);
|
||||
QCOMPARE(t->at(2).recrediteW, 0);
|
||||
QVERIFY(t->at(2).divergent);
|
||||
QCOMPARE(t->at(2).motif, QString(DecisionCode::RecreditSuspended));
|
||||
QCOMPARE(t->at(2).retenuW, t->at(0).consigneW);
|
||||
// Daté à l'instant de la commande (k=0) : deux cycles.
|
||||
QCOMPARE(t->at(2).refusDepuisS, 120);
|
||||
}
|
||||
|
||||
// ── Sans compteur : rien ne change. Relais sec : rien ne mesure, aucun écart publié ;
|
||||
// consigne continue : l'appareil mesure ce qu'il annonce, l'écart ne diverge jamais. ──
|
||||
for (int k = 0; k < r3.size(); ++k) {
|
||||
QVERIFY(!r3.at(k).commande);
|
||||
QVERIFY(r3.at(k).motif != QLatin1String(DecisionCode::RecreditSuspended));
|
||||
}
|
||||
for (int k = 1; k < v3.size(); ++k) {
|
||||
QVERIFY(v3.at(k).commande);
|
||||
QVERIFY(!v3.at(k).divergent);
|
||||
QVERIFY(v3.at(k).motif != QLatin1String(DecisionCode::RecreditSuspended));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief SG-Ready sous compteur dédié : la divergence se PUBLIE, le recrédit n'en dépend pas.
|
||||
*
|
||||
* Le recrédit d'une PAC est la puissance DÉCLARÉE de son état (invariant 8), pas sa mesure : il
|
||||
* ne consulte pas le refus, avant comme après le correctif. Ce qui change est ce que l'écart
|
||||
* publié dit d'une PAC qui démarre :
|
||||
* - **qui monte en puissance en plusieurs minutes** (1 000 W de plus par cycle) : en progression,
|
||||
* donc une latence — jamais divergente ;
|
||||
* - **qui ne démarre pas** (compteur figé à 0) : latence au cycle qui suit la commande (défaut de
|
||||
* 60 s), divergente au suivant. Le recrédit, lui, reste de 3 000 W.
|
||||
*/
|
||||
void Simulation::testSgReadyDivergenceIsPublishedOnly()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
constexpr double MaisonW = 300;
|
||||
struct Pas { int etat = 0; int recrediteW = 0; QString motif; bool divergent = false; bool commande = false; };
|
||||
auto rejouer = [&](const QString &nom, const std::function<double(int)> &pacW, int cycles) -> QList<Pas> {
|
||||
QList<Pas> trace;
|
||||
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-" + nom + ".json";
|
||||
QFile::remove(cfgPath);
|
||||
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||||
const QUuid meterId = addMeter(27330);
|
||||
m_experiencePlugin->energyManager()->setRootMeter(meterId);
|
||||
const QUuid compteurId = addMeter(27331);
|
||||
const QUuid k1 = addPowerSwitch(0, 27332);
|
||||
const QUuid k2 = addPowerSwitch(0, 27333);
|
||||
Thing *meter = tm->findConfiguredThing(meterId);
|
||||
Thing *compteur = tm->findConfiguredThing(compteurId);
|
||||
if (!arb || !meter || !compteur || !tm->findConfiguredThing(k1) || !tm->findConfiguredThing(k2))
|
||||
return trace;
|
||||
meter->setStateValue("connected", true);
|
||||
compteur->setStateValue("connected", true);
|
||||
compteur->setStateValue("currentPower", 0);
|
||||
QVariantMap cfg = sgReadyConfigMap("pac", "PAC", k1, k2, 1500.0, 3000.0, 0, 1);
|
||||
cfg.insert("meterThingId", compteurId.toString());
|
||||
LoadConfigStore *store = new LoadConfigStore(arb);
|
||||
arb->setLoadConfigStore(store);
|
||||
LoadConfigs cfgs;
|
||||
cfgs.append(LoadConfig::fromMap(cfg));
|
||||
QString err;
|
||||
if (!store->setConfigs(cfgs, &err))
|
||||
return trace;
|
||||
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
for (int k = 0; k < cycles; ++k) {
|
||||
const double tireW = pacW(k);
|
||||
compteur->setStateValue("currentPower", tireW);
|
||||
meter->setStateValue("currentPower", MaisonW + tireW - 5000.0); // 4 700 W sans la PAC
|
||||
arb->simulationCallUpdate(t0.addSecs(60 * k));
|
||||
QCoreApplication::processEvents();
|
||||
const QVariantMap snap = arb->loadTelemetry();
|
||||
const QVariantMap e = telemetrieParId(snap).value("pac");
|
||||
Pas p;
|
||||
p.etat = e.value("mechanism").toMap().value("state").toInt();
|
||||
p.recrediteW = qRound(snap.value("budget").toMap().value("recreditedW").toDouble());
|
||||
p.motif = e.value("decision").toMap().value("code").toString();
|
||||
p.commande = e.contains("command");
|
||||
p.divergent = e.value("command").toMap().value("divergent").toBool();
|
||||
trace.append(p);
|
||||
qInfo().noquote() << QStringLiteral("[%1] k=%2 état %3 | PAC %4 W | recrédit %5 | %6 | divergent %7")
|
||||
.arg(nom).arg(k).arg(p.etat).arg(qRound(tireW)).arg(p.recrediteW)
|
||||
.arg(p.motif).arg(p.divergent ? "oui" : "non");
|
||||
}
|
||||
qunsetenv("NYMEA_ENERGY_LOAD_CONFIG");
|
||||
QFile::remove(cfgPath);
|
||||
return trace;
|
||||
};
|
||||
|
||||
const QList<Pas> monte = rejouer("pac-monte", [](int k) { return qMin(3000.0, 1000.0 * k); }, 5);
|
||||
const QList<Pas> figee = rejouer("pac-figee", [](int) { return 0.0; }, 4);
|
||||
QCOMPARE(monte.size(), 5);
|
||||
QCOMPARE(figee.size(), 4);
|
||||
|
||||
for (const QList<Pas> *t : {&monte, &figee}) {
|
||||
QCOMPARE(t->at(0).etat, 4); // 4 700 W de surplus : état forcé
|
||||
for (int k = 1; k < t->size(); ++k) {
|
||||
QCOMPARE(t->at(k).etat, 4);
|
||||
QVERIFY(t->at(k).commande);
|
||||
// Le recrédit est la puissance DÉCLARÉE de l'état 4, refus ou pas.
|
||||
QCOMPARE(t->at(k).recrediteW, 3000);
|
||||
QVERIFY(t->at(k).motif != QLatin1String(DecisionCode::RecreditSuspended));
|
||||
}
|
||||
}
|
||||
// Une PAC qui monte en minutes est en progression : jamais divergente.
|
||||
for (int k = 1; k < monte.size(); ++k)
|
||||
QVERIFY2(!monte.at(k).divergent, qUtf8Printable(QStringLiteral("k=%1 : divergente").arg(k)));
|
||||
// Une PAC qui ne démarre pas : latence au cycle qui suit la commande, divergente ensuite.
|
||||
QVERIFY(!figee.at(1).divergent);
|
||||
QVERIFY(figee.at(2).divergent);
|
||||
QVERIFY(figee.at(3).divergent);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief P2 — un ARRÊT VERROUILLÉ engage le courant minimal : gelé au budget, et aucun faux refus.
|
||||
*
|
||||
|
||||
@ -269,6 +269,12 @@ private slots:
|
||||
void testRecreditIsALoanAgainstObedience();
|
||||
//! Une marche de consigne suivie ne fait plus suspendre le recrédit ; la consigne tient.
|
||||
void testRecreditSuspendedAfterAnObeyedStep();
|
||||
//! Un vrai refus reste vu au cycle qui suit la commande ; la descente n'a pas d'excuse.
|
||||
void testARealRefusalIsStillSeen();
|
||||
//! Relais et consignes sous compteur dédié : marche suivie, refus réel, sans compteur.
|
||||
void testWattLoadsFollowedOrRefusedUnderAMeter();
|
||||
//! SG-Ready sous compteur dédié : la divergence se publie, le recrédit n'en dépend pas.
|
||||
void testSgReadyDivergenceIsPublishedOnly();
|
||||
//! P2 — un arrêt verrouillé engage le courant minimal : gelé au budget, aucun faux refus.
|
||||
void testLockedStopFreezesTheMinimumAndIsNoRefusal();
|
||||
//! P2 — le verrou d'arrêt borne la somme éco + confort, il ne s'y ajoute pas.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user