feat(§10): ECO_FLOOR_MISSED, maintenant qu'il a un émetteur
Ce matin j'ai refusé de créer ce motif : il n'avait pas d'émetteur, et un motif qui naît avant son émetteur est un invariant posé sur du code que rien ne produit — un test toujours vert. La bascule de période l'a fait naître. Une obligation abandonnée est désormais un fait que le moteur produit, le journal le disait déjà, l'écran non. Trois causes, trois gestes : drawCap envoie regarder l'abonnement, loadCapacity la fiche technique de la charge, unmeasurable ce que la borne publie — et unavailable s'y ajoute. Sans la cause, l'écran n'affiche qu'un reproche sans recours. missingWh est ABSENT sous unmeasurable, jamais 0 : on ne sait pas ce qui a été livré, donc pas davantage ce qui manquait. Publier un chiffre inventerait la grandeur dont l'absence est le problème. R5 vérifiée contre la maquette plutôt que supposée conforme : la forme de `progress` correspond exactement. Un écart assumé et signalé : la maquette donnait ECO_FLOOR_GRID avec deliveredWh ; le moteur publie remainingWh. deliveredWh vit déjà dans `progress`, et le republier ferait deux sources pour un même fait. Surtout, remainingWh n'est PAS dérivable sous unmeasurable — où deliveredWh est absent — et c'est justement le cas où l'on achète. R8 : le point de rupture se lit sur DRAW_CAP, jamais sur un solde tombé à zéro. remainingW à 0 arrive aussi quand tout a été alloué normalement ; un écran qui le guetterait signalerait une rupture inexistante et manquerait la vraie. Écrit dans INTERFACE.md. Suite : simulation 58/58, loadmodel 22/22, charging 48/48 identique à la référence, spotmarket 32/32, doxygen 0 avertissement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
This commit is contained in:
parent
f2e99a7f50
commit
3e9e1a9bd2
@ -1,3 +1,32 @@
|
||||
powersync-energy-plugin-nymea (1.15.2+etm43) trixie; urgency=medium
|
||||
|
||||
* ECO_FLOOR_MISSED — le motif a désormais son ÉMETTEUR, et c'est pourquoi il est créé
|
||||
maintenant. Ce matin il n'en avait pas : le créer aurait posé un invariant sur un code que
|
||||
rien ne produit, un test toujours vert. La bascule de période (LM-1013-c) l'a fait naître —
|
||||
une obligation abandonnée est un fait que le moteur produit, et un fait produit se dit.
|
||||
* TROIS CAUSES, TROIS GESTES : `drawCap` envoie regarder l'abonnement, `loadCapacity` la fiche
|
||||
technique de la charge, `unmeasurable` ce que la borne publie. `unavailable` s'y ajoute
|
||||
(ECS-410). Sans la cause, l'écran n'affiche qu'un reproche sans recours.
|
||||
* `missingWh` ABSENT sous `unmeasurable`, jamais 0 : on ne sait pas ce qui a été livré, donc
|
||||
pas davantage ce qui manquait. Publier un chiffre inventerait la grandeur même dont
|
||||
l'absence EST le problème. Mesuré : « non tenue sur 2000 Wh — l'avancement n'était pas
|
||||
mesurable, et le manque n'est pas chiffrable ».
|
||||
* R5 VÉRIFIÉE contre la maquette plutôt que supposée conforme : `progress { regime, targetWh,
|
||||
deliveredWh absent si unmeasurable }` correspond exactement.
|
||||
* UN ÉCART ASSUMÉ, signalé plutôt qu'aligné en silence : la maquette donnait
|
||||
ECO_FLOOR_GRID { deliveredWh, targetWh, gridW } ; le moteur publie `remainingWh` au lieu de
|
||||
`deliveredWh`. Raison : `deliveredWh` vit déjà dans `progress`, et le republier ferait deux
|
||||
sources pour un même fait (LM-1012). Surtout, `remainingWh` n'est PAS dérivable sous
|
||||
`unmeasurable` — où `deliveredWh` est absent — et c'est justement le cas où l'on achète.
|
||||
* R8 — LE POINT DE RUPTURE SE LIT SUR `DRAW_CAP`, JAMAIS SUR UN SOLDE À ZÉRO. `remainingW` à 0
|
||||
arrive aussi quand tout a été alloué normalement : un écran qui le guetterait signalerait une
|
||||
rupture inexistante et manquerait la vraie. DRAW_CAP n'est émis que lorsqu'un plafond a
|
||||
réellement refusé, et il porte lequel. Écrit dans INTERFACE.md.
|
||||
* Suite : simulation 58/58, loadmodel 22/22, charging 48/48 identique à la référence,
|
||||
spotmarket 32/32, doxygen 0 avertissement.
|
||||
|
||||
-- Patrick Schurig <etm.schurig@gmail.com> Mon, 31 Aug 2026 11:00:00 +0200
|
||||
|
||||
powersync-energy-plugin-nymea (1.15.2+etm42) trixie; urgency=medium
|
||||
|
||||
* §10 étape D (2/2) — LA PASSE ÉCO EXISTE. Deux passes sur la MÊME liste, MÊME ordre ; ce qui
|
||||
|
||||
@ -452,6 +452,14 @@ namespace {
|
||||
constexpr double BruitAvancementWh = 50.0;
|
||||
}
|
||||
|
||||
void EnergyArbitrator::noterCauseEco(const QString &loadId, const QString &cause) const
|
||||
{
|
||||
if (cause.isEmpty())
|
||||
m_ecoCause.remove(loadId);
|
||||
else
|
||||
m_ecoCause.insert(loadId, cause);
|
||||
}
|
||||
|
||||
EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc, const QDateTime &now) const
|
||||
{
|
||||
EcoProgress p;
|
||||
@ -474,6 +482,26 @@ EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc, const QDateTime
|
||||
const QDateTime echeance = resolveDeadline(lc.needs, now);
|
||||
if (echeance.isValid() && m_ecoBaseEcheance.value(lc.id) != echeance) {
|
||||
if (m_ecoBaseEcheance.contains(lc.id)) {
|
||||
// CLÔTURE — la période précédente s'achève. Si elle n'était pas tenue, c'est un FAIT
|
||||
// que le moteur produit, et un fait produit doit se dire. Ce motif n'existait pas ce
|
||||
// matin faute d'émetteur ; la bascule en est un.
|
||||
const bool mesurait = lc.telemetry.sessionMeasurable && m_ecoBaseWh.contains(lc.id);
|
||||
const double livre = mesurait
|
||||
? qMax(0.0, lc.telemetry.sessionWh - m_ecoBaseWh.value(lc.id)) : 0.0;
|
||||
if (!mesurait || livre < p.targetWh) {
|
||||
p.closedMissed = true;
|
||||
p.closedKnown = mesurait;
|
||||
// Le manque n'est chiffré que s'il est CONNU. Sous un régime non mesuré, on ne
|
||||
// sait pas ce qui a été livré, donc pas davantage ce qui manquait : publier un
|
||||
// chiffre inventerait la grandeur même dont l'absence est le problème.
|
||||
if (mesurait)
|
||||
p.closedMissingWh = p.targetWh - livre;
|
||||
p.closedCause = mesurait
|
||||
? (m_ecoCause.value(lc.id).isEmpty()
|
||||
? QString::fromLatin1(MissedCause::LoadCapacity) // servie sans blocage
|
||||
: m_ecoCause.value(lc.id)) // et servie mais pas assez
|
||||
: QString::fromLatin1(MissedCause::Unmeasurable);
|
||||
}
|
||||
// L'obligation précédente s'achève. Si elle n'était pas tenue, elle est abandonnée
|
||||
// ici — SANS motif dédié à ce stade, et c'est une limite assumée : le dire demande
|
||||
// un `ECO_FLOOR_MISSED` que rien n'émet encore. Le journal, lui, le dit.
|
||||
@ -483,6 +511,7 @@ EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc, const QDateTime
|
||||
}
|
||||
m_ecoBaseEcheance.insert(lc.id, echeance);
|
||||
m_ecoBaseWh.remove(lc.id);
|
||||
m_ecoCause.remove(lc.id);
|
||||
}
|
||||
|
||||
// C1 de LM-1009 — la borne ne compte pas : l'obligation n'est PAS mesurable, et ça se dit.
|
||||
|
||||
@ -379,6 +379,17 @@ public:
|
||||
*/
|
||||
EcoProgress ecoProgress(const LoadContext &lc, const QDateTime &now) const;
|
||||
|
||||
/*!
|
||||
* \brief Note pourquoi l'obligation d'une charge n'a pas pu être servie ce cycle.
|
||||
* \param loadId Charge. \param cause Code de \c MissedCause, vide si elle a été servie.
|
||||
*
|
||||
* \note Appelé par la passe éco à chaque cycle. À la CLÔTURE d'une période, c'est la
|
||||
* dernière cause notée qui est publiée : celle qui valait au dernier cycle où l'on aurait
|
||||
* encore pu servir. Retenir la plus fréquente serait plus juste en moyenne et moins utile
|
||||
* en pratique — ce qu'on veut savoir est ce qui bloquait à la fin.
|
||||
*/
|
||||
void noterCauseEco(const QString &loadId, const QString &cause) const;
|
||||
|
||||
/*!
|
||||
* \brief [L4] Déleste les charges du waterfall tant que le plafond de tirage est dépassé.
|
||||
*
|
||||
@ -733,6 +744,9 @@ private:
|
||||
//! quotidienne franchie), la période d'obligation change : la base se reprend, sinon le
|
||||
//! lendemain hérite de l'énergie de la veille et se croit déjà tenu.
|
||||
mutable QHash<QString, QDateTime> m_ecoBaseEcheance;
|
||||
//! loadId → dernière cause de non-service notée par la passe éco (\c MissedCause). Vide
|
||||
//! quand la charge a été servie. Publiée à la clôture d'une période non tenue.
|
||||
mutable QHash<QString, QString> m_ecoCause;
|
||||
ThingManager *m_tm = nullptr; //!< ThingManager (pour construire les adaptateurs config).
|
||||
LoadConfigStore *m_loadConfigStore = nullptr; //!< Store config charge pilotée (non adopté).
|
||||
|
||||
|
||||
@ -258,6 +258,30 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
|
||||
eco.hasProgress = true;
|
||||
eco.progress = avancement;
|
||||
|
||||
// CLÔTURE NON TENUE — l'événement du cycle, et il prime sur ce que la période neuve
|
||||
// demande. Ce cycle-ci dit ce qui vient de se perdre ; le suivant repart sur la période
|
||||
// ouverte. Le taire laisserait l'écran muet sur le seul instant où l'information existe.
|
||||
if (avancement.closedMissed) {
|
||||
QVariantMap params{{"targetWh", qRound(avancement.targetWh)},
|
||||
{"cause", avancement.closedCause}};
|
||||
// `missingWh` ABSENT quand le manque n'est pas chiffrable, jamais 0 : un zéro se
|
||||
// lirait « il ne manquait rien », là où la vérité est « on n'en sait rien ».
|
||||
if (avancement.closedKnown)
|
||||
params.insert(QStringLiteral("missingWh"), qRound(avancement.closedMissingWh));
|
||||
eco.targetW = 0;
|
||||
eco.counts[CountKey::BudgetAllocated] = 0;
|
||||
eco.reason = { DecisionCode::EcoFloorMissed, params };
|
||||
niveauxEco.insert(lc.id, eco);
|
||||
continue;
|
||||
}
|
||||
|
||||
// La charge est-elle seulement commandable ? Une indisponibilité empêche de servir
|
||||
// l'obligation, et c'est une cause à part — on ne sait plus la commander (ECS-410).
|
||||
if (!lc.telemetry.available) {
|
||||
m_arbitrator->noterCauseEco(lc.id, QString::fromLatin1(MissedCause::Unavailable));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cible.tenue) {
|
||||
// R6 — `ECO_FLOOR_MET` ne peut PAS sortir sous un régime non mesuré, et
|
||||
// `computeEcoTarget()` ne conclut jamais « tenue » dans ce cas : la garde est ici
|
||||
@ -276,8 +300,21 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
|
||||
// un droit de faire disjoncter : si l'autorisation est épuisée, on n'achète pas, et
|
||||
// c'est le délestage qui rattrapera ce qui a déjà été engagé.
|
||||
double achatW = cible.watts;
|
||||
const bool bridePlafond = margeRestante.borne && achatW > qMax(0.0, margeRestante.watts);
|
||||
if (margeRestante.borne)
|
||||
achatW = qBound(0.0, achatW, qMax(0.0, margeRestante.watts));
|
||||
|
||||
// La CAUSE se note à chaque cycle, servie ou non. À la clôture, c'est la dernière qui
|
||||
// est publiée : celle qui bloquait au dernier moment où l'on aurait encore pu servir.
|
||||
if (bridePlafond)
|
||||
m_arbitrator->noterCauseEco(lc.id, QString::fromLatin1(MissedCause::DrawCap));
|
||||
else if (lc.declared.maxPowerW > 0 && cible.watts >= lc.declared.maxPowerW)
|
||||
// La cadence demandée atteint le plafond PHYSIQUE de la charge : ce n'est plus nous
|
||||
// qui bridons, c'est elle qui ne peut pas suivre. Deux gestes différents.
|
||||
m_arbitrator->noterCauseEco(lc.id, QString::fromLatin1(MissedCause::LoadCapacity));
|
||||
else
|
||||
m_arbitrator->noterCauseEco(lc.id, QString()); // servie sans blocage
|
||||
|
||||
if (achatW <= 0)
|
||||
continue; // rien d'achetable ce cycle : pas de niveau éco plutôt qu'un zéro forgé
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
|
||||
#include "decisionreason.h"
|
||||
#include "ecoprogress.h"
|
||||
#include <QStringList>
|
||||
|
||||
/*
|
||||
@ -134,6 +135,23 @@ QString renderFr(const DecisionReason &reason, const QString &label)
|
||||
"livrer en %4 (cadence recalculée à chaque cycle)")
|
||||
.arg(label).arg(w("gridW")).arg(w("remainingWh")).arg(delai);
|
||||
}
|
||||
if (c == QLatin1String(DecisionCode::EcoFloorMissed)) {
|
||||
const QString cause = p.value(QStringLiteral("cause")).toString();
|
||||
const QString pourquoi =
|
||||
cause == QLatin1String(MissedCause::DrawCap) ? QStringLiteral("l'autorisation de soutirage était épuisée")
|
||||
: cause == QLatin1String(MissedCause::LoadCapacity) ? QStringLiteral("la charge ne pouvait pas absorber la cadence")
|
||||
: cause == QLatin1String(MissedCause::Unavailable) ? QStringLiteral("la charge était indisponible")
|
||||
: cause == QLatin1String(MissedCause::Unmeasurable) ? QStringLiteral("l'avancement n'était pas mesurable")
|
||||
: QStringLiteral("cause inconnue");
|
||||
// Le manque n'est chiffré que s'il est connu. Sous un régime non mesuré, l'annoncer à
|
||||
// zéro ferait lire « il ne manquait rien » là où la vérité est « on n'en sait rien ».
|
||||
if (!p.contains(QStringLiteral("missingWh")))
|
||||
return QStringLiteral("Obligation de %1 non tenue sur %2 Wh — %3, et le manque n'est "
|
||||
"pas chiffrable")
|
||||
.arg(label).arg(w("targetWh")).arg(pourquoi);
|
||||
return QStringLiteral("Obligation de %1 non tenue : il manquait %2 Wh sur %3 — %4")
|
||||
.arg(label).arg(w("missingWh")).arg(w("targetWh")).arg(pourquoi);
|
||||
}
|
||||
if (c == QLatin1String(DecisionCode::EcoFloorMet))
|
||||
return QStringLiteral("Obligation de %1 tenue : %2 Wh livrés sur %3 — plus rien à acheter")
|
||||
.arg(label).arg(w("deliveredWh")).arg(w("targetWh"));
|
||||
|
||||
@ -212,6 +212,29 @@ constexpr const char *EcoFloorGrid = "ECO_FLOOR_GRID";
|
||||
*/
|
||||
constexpr const char *EcoFloorMet = "ECO_FLOOR_MET";
|
||||
|
||||
/*!
|
||||
* \brief Une période d'obligation s'est close SANS avoir été tenue.
|
||||
*
|
||||
* params : \c targetWh (l'obligation), \c cause (code opaque de \c MissedCause),
|
||||
* \c o:missingWh (ce qui manquait — **absent** quand l'avancement n'était pas mesurable).
|
||||
*
|
||||
* \par Pourquoi ce motif n'existait pas ce matin
|
||||
* Il n'avait **pas d'émetteur**. Le créer alors aurait posé un invariant sur un code que rien ne
|
||||
* produit : un test toujours vert, donc un test qui ne protège rien. La bascule de période
|
||||
* (\rule{LM-1013-c}) l'a fait naître — une obligation abandonnée est désormais un fait que le
|
||||
* moteur produit, et un fait produit doit se dire.
|
||||
*
|
||||
* \par Trois causes, trois gestes
|
||||
* `drawCap` envoie regarder l'abonnement ou les autres consommateurs ; `loadCapacity` envoie
|
||||
* lire la fiche technique de la charge ; `unmeasurable` envoie regarder ce que la borne publie.
|
||||
* Sans la cause, l'écran ne peut afficher qu'un reproche sans recours.
|
||||
*
|
||||
* \note \c missingWh est **absent** sous \c unmeasurable, jamais 0. On ne sait pas ce qui a été
|
||||
* livré, donc pas davantage ce qui manquait : publier un chiffre inventerait la grandeur même
|
||||
* dont l'absence est le problème. Même règle que \c deliveredWh (\rule{LM-1009} C1).
|
||||
*/
|
||||
constexpr const char *EcoFloorMissed = "ECO_FLOOR_MISSED";
|
||||
|
||||
// --- SG-Ready (PAC) ---------------------------------------------------------
|
||||
//! params: \c state — le verrou minStateHold maintient l'état (court-cycling compresseur).
|
||||
constexpr const char *LockMinStateHold = "LOCK_MIN_STATE_HOLD";
|
||||
|
||||
@ -42,4 +42,35 @@ struct EcoProgress {
|
||||
|
||||
//! Énergie à livrer (Wh), déclarée par \c needs. 0 = aucune obligation en cours.
|
||||
double targetWh = 0;
|
||||
|
||||
// --- Clôture de période (LM-1014-b) : vrai UN SEUL CYCLE, celui de la bascule -----------
|
||||
|
||||
//! \brief Une période d'obligation vient de se clore SANS avoir été tenue.
|
||||
//! Événement d'un cycle, pas un état : le suivant repart sur la période neuve.
|
||||
bool closedMissed = false;
|
||||
|
||||
//! \brief Ce qui manquait à la clôture (Wh). Sans objet si \c !closedKnown.
|
||||
double closedMissingWh = 0;
|
||||
|
||||
//! \brief Vrai si le manque est CHIFFRABLE. Faux sous un régime non mesuré : on ne sait pas
|
||||
//! ce qui a été livré, donc pas davantage ce qui manquait. Publier un chiffre là serait
|
||||
//! inventer la grandeur même dont l'absence est le problème.
|
||||
bool closedKnown = false;
|
||||
|
||||
//! \brief Pourquoi elle n'a pas été tenue — code opaque de \c MissedCause.
|
||||
QString closedCause;
|
||||
};
|
||||
|
||||
//! \brief Causes de non-tenue publiées par \c ECO_FLOOR_MISSED. Opaques, comparées jamais
|
||||
//! traduites. Chacune appelle un geste différent, et c'est leur raison d'être.
|
||||
namespace MissedCause {
|
||||
//! L'autorisation de soutirage était épuisée : on n'avait pas le DROIT d'acheter davantage.
|
||||
constexpr const char *DrawCap = "drawCap";
|
||||
//! La charge ne pouvait pas absorber la cadence demandée — son plafond physique, pas le nôtre.
|
||||
constexpr const char *LoadCapacity = "loadCapacity";
|
||||
//! L'avancement n'a jamais été mesurable : on ne peut affirmer ni qu'elle est tenue, ni de
|
||||
//! combien elle manque. C'est le seul cas où \c closedKnown est faux.
|
||||
constexpr const char *Unmeasurable = "unmeasurable";
|
||||
//! La charge était indisponible : on ne savait plus la commander (ECS-410).
|
||||
constexpr const char *Unavailable = "unavailable";
|
||||
}
|
||||
|
||||
@ -6915,6 +6915,101 @@ void Simulation::testEcoPassBuysAndSaysTheTrajectory()
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief [§10] Une obligation abandonnée est un FAIT produit — et un fait produit se dit.
|
||||
*
|
||||
* \par Ce motif n'existait pas le matin même
|
||||
* Il n'avait pas d'ÉMETTEUR : le créer alors aurait posé un invariant sur un code que rien ne
|
||||
* produit — un test toujours vert, donc un test qui ne protège rien. La bascule de période
|
||||
* (\rule{LM-1013-c}) l'a fait naître. Le journal disait déjà l'abandon ; l'écran, non.
|
||||
*
|
||||
* \par Trois causes, trois gestes
|
||||
* `drawCap` envoie regarder l'abonnement ou les autres consommateurs ; `loadCapacity` envoie
|
||||
* lire la fiche technique de la charge ; `unmeasurable` envoie regarder ce que la borne publie.
|
||||
* Sans la cause, l'écran n'affiche qu'un reproche sans recours.
|
||||
*
|
||||
* \warning **Ce test devient faux** si l'obligation cesse d'être bornée par l'autorisation de
|
||||
* soutirage — alors `drawCap` ne serait plus une cause possible, et c'est le cas 1 qu'il
|
||||
* faudrait réécrire, pas le moteur.
|
||||
*/
|
||||
void Simulation::testEcoFloorMissedSaysWhatWasLostAndWhy()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
|
||||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
QVERIFY(arb);
|
||||
RuleBasedScheduler sched(arb);
|
||||
|
||||
auto cycle = [&](const QString &id, const QDateTime &at, bool mesurable,
|
||||
double livreWh, double margeW) -> LoadAction {
|
||||
SurplusContext ctx;
|
||||
ctx.timestamp = at;
|
||||
ctx.meter.importW = 500;
|
||||
ctx.drawMargin.borne = true;
|
||||
ctx.drawMargin.watts = margeW;
|
||||
ctx.drawMargin.source = DrawCapSource::Breaker;
|
||||
ctx.drawMargin.phase = 0;
|
||||
|
||||
LoadContext ecs;
|
||||
ecs.id = id;
|
||||
ecs.label = "ECS";
|
||||
ecs.adapter = "etmvariableload";
|
||||
ecs.priority = 1;
|
||||
ecs.declared.maxPowerW = 3000;
|
||||
ecs.needs.minEnergyWhPerDay = 2000;
|
||||
ecs.needs.dailyDeadline = "07:00";
|
||||
ecs.telemetry.sessionMeasurable = mesurable;
|
||||
ecs.telemetry.sessionWh = livreWh;
|
||||
ctx.loads.append(ecs);
|
||||
return sched.getPlan(ctx).timeSlots.first().actions.first();
|
||||
};
|
||||
|
||||
const QDateTime avant = utcDateTime(QDate(2026, 8, 31), QTime(5, 0, 0));
|
||||
const QDateTime apres = utcDateTime(QDate(2026, 8, 31), QTime(7, 30, 0));
|
||||
|
||||
// --- Cas 1 : MESURÉ, bridé par l'autorisation de soutirage ------------------------------
|
||||
// Marge à 200 W alors que la cadence en demande 1000 : on note `drawCap`, et à la bascule
|
||||
// c'est cette cause-là qui est publiée.
|
||||
cycle("ecs-cap", avant, true, 0, 200);
|
||||
cycle("ecs-cap", avant, true, 300, 200); // 300 Wh livrés sur 2000
|
||||
const LoadAction close = cycle("ecs-cap", apres, true, 300, 200);
|
||||
QCOMPARE(close.levels.at(0).reason.code, QString(DecisionCode::EcoFloorMissed));
|
||||
const QVariantMap pc = close.levels.at(0).reason.params;
|
||||
QCOMPARE(pc.value("cause").toString(), QString(MissedCause::DrawCap));
|
||||
QCOMPARE(pc.value("missingWh").toInt(), 1700); // 2000 dus, 300 livrés
|
||||
QCOMPARE(pc.value("targetWh").toInt(), 2000);
|
||||
// Le TEXTE porte le manque ET le geste. « Non tenue » seul serait un reproche sans recours.
|
||||
const QString tc = renderFr(close.levels.at(0).reason, "ECS");
|
||||
QVERIFY2(tc.contains("1700"), qUtf8Printable("motif: " + tc));
|
||||
QVERIFY2(tc.contains("soutirage"), qUtf8Printable("motif: " + tc));
|
||||
|
||||
// --- Cas 2 : NON MESURÉ -----------------------------------------------------------------
|
||||
cycle("ecs-inc", avant, false, 0, 5000);
|
||||
const LoadAction closeInc = cycle("ecs-inc", apres, false, 0, 5000);
|
||||
QCOMPARE(closeInc.levels.at(0).reason.code, QString(DecisionCode::EcoFloorMissed));
|
||||
const QVariantMap pi = closeInc.levels.at(0).reason.params;
|
||||
QCOMPARE(pi.value("cause").toString(), QString(MissedCause::Unmeasurable));
|
||||
// `missingWh` ABSENT, jamais 0. On ne sait pas ce qui a été livré, donc pas davantage ce
|
||||
// qui manquait : un zéro se lirait « il ne manquait rien », là où la vérité est « on n'en
|
||||
// sait rien ». Publier un chiffre inventerait la grandeur dont l'absence EST le problème.
|
||||
QVERIFY2(!pi.contains("missingWh"),
|
||||
"sous unmeasurable, le manque n'est pas chiffrable — pas de zéro forgé");
|
||||
const QString ti = renderFr(closeInc.levels.at(0).reason, "ECS");
|
||||
QVERIFY2(ti.contains("pas chiffrable"), qUtf8Printable("motif: " + ti));
|
||||
|
||||
// --- Cas 3 : obligation TENUE — aucune clôture manquée ----------------------------------
|
||||
cycle("ecs-ok", avant, true, 0, 5000);
|
||||
const LoadAction closeOk = cycle("ecs-ok", apres, true, 2500, 5000);
|
||||
QVERIFY2(closeOk.levels.at(0).reason.code != QString(DecisionCode::EcoFloorMissed),
|
||||
"une obligation tenue ne produit pas de motif de non-tenue");
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testCountsSumToTarget()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
|
||||
@ -196,6 +196,7 @@ private slots:
|
||||
void testDrawAuthorisationSurvivesTheBatteryReserve();
|
||||
void testEcoProgressBaseAndRegime();
|
||||
void testEcoPassBuysAndSaysTheTrajectory();
|
||||
void testEcoFloorMissedSaysWhatWasLostAndWhy();
|
||||
void testARefusedStateIsDistinguishableFromAnApplied();
|
||||
void testL4ShedsAPacAndSaysItForcedTheLock();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user