fix: la contradiction progressMeasurable/regime, et la garde qui la rend sûre
Le premier dit ce que le MATÉRIEL permet, le second ce que le MOTEUR fait. Seul l'EvAdapter remplissait sessionMeasurable, donc sur toute charge non-VE le premier disait oui et le second non — et l'avertissement de saisie de l'app, qui se déclenche sur progressMeasurable, restait muet exactement sur les charges achetées à l'aveugle. sessionWh est alimenté depuis le compteur désigné. L'avertissement de l'app est arrivé AVANT que j'écrive la garde, et il était décisif. Un compteur qui disparaît peut publier 0 au lieu de se taire : la V2C a forgé un zéro sur currentPower et ses booléens, mais laissé sessionEnergy à sa dernière valeur. La politique dépend du plugin — donc inutilisable comme garde. Et le point à vérifier avait une réponse nette : il n'existe AUCUN signal de joignabilité fiable. setupComplete parle du setup et non du lien ; connected est maintenu par le plugin, la V2C basculant après trois échecs quand un compteur générique peut ne jamais basculer. La garde ne pouvait pas reposer là-dessus. Elle porte donc sur la SÉMANTIQUE du compteur, seule chose que le moteur sache sans parier : sur un compteur de session un recul est une session neuve, sur un compteur de vie c'est une anomalie — on gèle, on ne recapture jamais. Sans elle, le scénario chiffré sort tel quel : base reprise à 0 pendant l'absence, retour avec 10 kWh réels, deliveredWh à 10 000 en un cycle, obligation déclarée tenue sans rien de livré. Et R6 ne l'attrape pas : elle n'interdit ECO_FLOOR_MET que sous unmeasurable, alors que ce chemin est MESURÉ — c'est le seul par lequel un faux « tenue » peut sortir, et c'est celui qu'ouvrait l'alimentation depuis un compteur. audit-controles.py dit maintenant en tête qu'il ne tranche pas et ne doit jamais trancher : neuf des dix identités relevées contrôlaient réellement, et un verdict automatique aurait supprimé neuf contrôles utiles. AGENTS.md : la diversité du mock n'est pas un confort de test, c'est une borne supérieure sur ce que la suite peut prouver — elle se fixe en écrivant les classes, pas les tests. Quatrième occurrence du piège d'insertion doxygen, rattrapée par le même contrôle. Comme annoncé. Suite : simulation 63/63, 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
e7485e76f9
commit
a510202ac7
@ -1,3 +1,39 @@
|
||||
powersync-energy-plugin-nymea (1.15.2+etm51) trixie; urgency=medium
|
||||
|
||||
* URGENT — CONTRADICTION SUPPRIMÉE entre `progressMeasurable` et `regime`. Le premier dit ce
|
||||
que le MATÉRIEL permet, le second ce que le MOTEUR fait ; seul l'EvAdapter remplissait
|
||||
`sessionMeasurable`, donc sur toute charge non-VE le premier disait oui et le second non.
|
||||
L'avertissement de saisie de l'app se déclenchant sur `progressMeasurable == false`, il
|
||||
restait MUET exactement sur les charges achetées à l'aveugle. `sessionWh` est désormais
|
||||
alimenté depuis le compteur DÉSIGNÉ pour les charges non-VE.
|
||||
* LM-1012-d — UN ACCUMULATEUR NE PARIE JAMAIS SUR CE QU'UN PLUGIN FAIT DE SES ÉTATS EN TOMBANT.
|
||||
Avertissement de l'app intégré AVANT l'écriture, et il était décisif : un compteur qui
|
||||
disparaît peut publier 0 au lieu de se taire. Mesuré sur la V2C — `currentPower` 4 396 → 0 et
|
||||
les booléens à false, mais `sessionEnergy` resté à sa dernière valeur. La politique dépend du
|
||||
plugin, ce qui la rend inutilisable comme garde.
|
||||
* ET IL N'EXISTE AUCUN SIGNAL DE JOIGNABILITÉ FIABLE — vérifié : `setupComplete` parle du setup
|
||||
et non du lien, `connected` est maintenu par le plugin (V2C après trois échecs, un compteur
|
||||
générique peut ne jamais basculer). La garde ne pouvait donc pas reposer là-dessus.
|
||||
* LA GARDE PORTE SUR LA SÉMANTIQUE DU COMPTEUR, seule chose que le moteur sache sans parier :
|
||||
un recul sur compteur de SESSION est une session neuve (recapture) ; sur compteur de VIE,
|
||||
c'est une anomalie (GEL, jamais de recapture). Sans elle : base reprise à 0 pendant
|
||||
l'absence, retour du compteur avec 10 kWh réels, `deliveredWh` saute à 10 000 en un cycle et
|
||||
l'obligation se déclare TENUE sans que rien n'ait été livré. R6 ne l'attrape pas — elle
|
||||
n'interdit ECO_FLOOR_MET que sous `unmeasurable`, et ce chemin-là est MESURÉ. Contre-épreuve :
|
||||
garde retirée, le régime reste « measured » pendant la panne.
|
||||
* `tools/audit-controles.py` — écrit en tête qu'il NE TRANCHE PAS et ne doit jamais trancher :
|
||||
au balayage, neuf des dix identités relevées contrôlaient réellement. Un outil qui aurait
|
||||
rendu un verdict aurait supprimé neuf contrôles utiles.
|
||||
* AGENTS.md — la diversité du mock n'est pas un confort de test, c'est une BORNE SUPÉRIEURE sur
|
||||
ce que la suite peut prouver, et elle se fixe en écrivant les classes, pas les tests.
|
||||
Troisième occurrence documentée.
|
||||
* Quatrième occurrence du piège d'insertion doxygen, rattrapée par le même contrôle — comme
|
||||
annoncé.
|
||||
* Suite : simulation 63/63, loadmodel 22/22, charging 48/48 identique à la référence,
|
||||
spotmarket 32/32, doxygen 0 avertissement.
|
||||
|
||||
-- Patrick Schurig <etm.schurig@gmail.com> Thu, 03 Sep 2026 15:00:00 +0200
|
||||
|
||||
powersync-energy-plugin-nymea (1.15.2+etm50) trixie; urgency=medium
|
||||
|
||||
* BALAYAGE — « qu'est-ce que ce contrôle aurait attrapé s'il avait échoué ? » passée à toute la
|
||||
|
||||
@ -585,7 +585,25 @@ EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc, const QDateTime
|
||||
// réelle, et l'écran ne pourrait plus dire ce qui reste dû.
|
||||
if (!m_ecoMesureDepuis.contains(lc.id))
|
||||
m_ecoMesureDepuis.insert(lc.id, now);
|
||||
} else if (sessionWh < m_ecoBaseWh.value(lc.id) - BruitAvancementWh) {
|
||||
} else if (lc.telemetry.sessionCumulative
|
||||
&& sessionWh < m_ecoBaseWh.value(lc.id) - BruitAvancementWh) {
|
||||
// COMPTEUR DE VIE QUI RECULE — impossible physiquement, donc la valeur ne vaut rien :
|
||||
// zéro forgé à la perte du lien, ou compteur remplacé. On NE RECAPTURE PAS, et on gèle
|
||||
// l'avancement le temps de l'anomalie, comme si la mesure avait disparu.
|
||||
//
|
||||
// Recapturer ici serait le chemin du faux « tenue » : base reprise à 0 pendant
|
||||
// l'absence, retour du compteur avec son cumul réel, et l'obligation se déclarerait
|
||||
// tenue sans que rien n'ait été livré. R6 ne l'attraperait pas — elle n'interdit
|
||||
// ECO_FLOOR_MET que sous `unmeasurable`, et ce chemin-là est MESURÉ.
|
||||
qCInfo(dcNymeaEnergy()) << "[Arbitre] Avancement éco" << lc.id
|
||||
<< "— compteur de VIE en recul ("
|
||||
<< m_ecoBaseWh.value(lc.id) << "→" << sessionWh
|
||||
<< " Wh) : valeur ignorée, avancement gelé.";
|
||||
p.regime = QString::fromLatin1(ProgressRegime::Unmeasurable);
|
||||
p.purchasedWh = m_ecoAchatWh.value(lc.id, 0.0);
|
||||
return p; // le tronçon reste OUVERT sur sa dernière valeur saine
|
||||
} else if (!lc.telemetry.sessionCumulative
|
||||
&& sessionWh < m_ecoBaseWh.value(lc.id) - BruitAvancementWh) {
|
||||
// RECUL FRANC de l'accumulateur : le plugin l'a remis à la valeur brute, donc une
|
||||
// nouvelle session s'est ouverte. On recapture, sinon la différence devient négative.
|
||||
// Ce n'est PAS la détection de remise à zéro de la borne — celle-là vit dans le plugin,
|
||||
@ -1030,6 +1048,40 @@ bool EnergyArbitrator::evSurplusPlannedByWaterfall(EvCharger *evCharger) const
|
||||
return evParticipatesInWaterfall(evCharger);
|
||||
}
|
||||
|
||||
void EnergyArbitrator::alimenterAvancement(LoadContext &lc) const
|
||||
{
|
||||
// CONTRADICTION CORRIGÉE (2026-09-03, remontée par l'app). `progressMeasurable` dit ce que
|
||||
// le MATÉRIEL permet ; `regime` dit ce que le MOTEUR fait. Seul l'EvAdapter remplissait
|
||||
// `sessionMeasurable`, si bien que toute charge non-VE était `unmeasurable` alors que sa
|
||||
// configuration annonçait l'inverse.
|
||||
//
|
||||
// Ce n'est pas qu'une incohérence d'affichage : l'avertissement de saisie de l'app se
|
||||
// déclenche sur `progressMeasurable == false`, donc il restait MUET exactement sur les
|
||||
// charges achetées à l'aveugle — celles dont l'obligation ne pouvait pas être suivie.
|
||||
if (lc.telemetry.sessionMeasurable || !m_tm || !m_loadConfigStore)
|
||||
return; // déjà renseigné par l'adaptateur (VE), ou rien pour le résoudre
|
||||
|
||||
for (const LoadConfig &c : m_loadConfigStore->configs()) {
|
||||
if (c.id() != lc.id || c.meterThingId().isEmpty())
|
||||
continue;
|
||||
Thing *compteur = m_tm->findConfiguredThing(ThingId(c.meterThingId()));
|
||||
// Il faut un CUMUL, pas une puissance : `currentPower` ne dit rien de ce qui a été
|
||||
// livré. C'est la même exigence que celle publiée en `progressUnmeasurableCause`, et
|
||||
// elle doit être la MÊME des deux côtés — sinon la contradiction se déplace au lieu de
|
||||
// disparaître.
|
||||
if (compteur && compteur->thingClass().hasStateType("totalEnergyConsumed")) {
|
||||
lc.telemetry.sessionMeasurable = true;
|
||||
// Compteur de VIE : un recul y est une ANOMALIE, jamais une nouvelle session.
|
||||
lc.telemetry.sessionCumulative = true;
|
||||
// Compteur de VIE, pas de session — et c'est sans importance : l'avancement est une
|
||||
// DIFFÉRENCE depuis une base capturée à l'ouverture de l'obligation (LM-1012-b).
|
||||
// Un cumul monotone est même plus sûr qu'un compteur de session, qui recule.
|
||||
lc.telemetry.sessionWh = compteur->stateValue("totalEnergyConsumed").toDouble() * 1000.0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void EnergyArbitrator::qualifierRefus(LoadContext &lc, const QDateTime &now) const
|
||||
{
|
||||
const QDateTime depuis = m_divergenceDepuis.value(lc.id);
|
||||
@ -1142,6 +1194,7 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
|
||||
continue;
|
||||
}
|
||||
LoadContext lcq = it.value()->toLoadContext(now);
|
||||
alimenterAvancement(lcq);
|
||||
qualifierRefus(lcq, now);
|
||||
ctx.loads.append(lcq);
|
||||
}
|
||||
@ -1171,7 +1224,8 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
|
||||
lc.telemetry.lockMinPowerW = 0; // rien d'engagé : rien à geler au budget
|
||||
lc.telemetry.lockMaxPowerW = -1;
|
||||
LoadContext lcq = lc;
|
||||
qualifierRefus(lcq, now);
|
||||
alimenterAvancement(lcq);
|
||||
qualifierRefus(lcq, now);
|
||||
ctx.loads.append(lcq);
|
||||
}
|
||||
}
|
||||
@ -1180,6 +1234,7 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
|
||||
// sg-ready) — une seule table, toutes construites depuis la configuration persistée. ---
|
||||
for (auto it = m_loadAdapters.constBegin(); it != m_loadAdapters.constEnd(); ++it) {
|
||||
LoadContext lcq = it.value()->toLoadContext(now);
|
||||
alimenterAvancement(lcq);
|
||||
qualifierRefus(lcq, now);
|
||||
ctx.loads.append(lcq);
|
||||
}
|
||||
|
||||
@ -705,6 +705,23 @@ private:
|
||||
*/
|
||||
void qualifierRefus(LoadContext &lc, const QDateTime &now) const;
|
||||
|
||||
/*!
|
||||
* \brief Alimente l'avancement d'une charge NON-VE depuis son compteur désigné.
|
||||
* \param[in,out] lc Charge du cycle.
|
||||
*
|
||||
* \par La contradiction que cette méthode supprime
|
||||
* \c progressMeasurable dit ce que le MATÉRIEL permet ; \c regime dit ce que le MOTEUR fait.
|
||||
* Seul l'\c EvAdapter remplissait \c sessionMeasurable — donc sur toute charge non-VE le
|
||||
* premier disait oui et le second non. L'avertissement de saisie de l'app se déclenchant sur
|
||||
* \c progressMeasurable, il restait **muet exactement sur les charges achetées à l'aveugle**.
|
||||
*
|
||||
* \note Il faut un CUMUL (\c totalEnergyConsumed), pas une puissance : \c currentPower ne dit
|
||||
* rien de ce qui a été livré. Même exigence que \c progressUnmeasurableCause, et elle doit
|
||||
* être la même des deux côtés, sinon la contradiction se déplace au lieu de disparaître.
|
||||
*/
|
||||
void alimenterAvancement(LoadContext &lc) const;
|
||||
|
||||
|
||||
|
||||
/*!
|
||||
* \brief Publie l'écart entre ce qu'on a COMMANDÉ et ce qui est MESURÉ.
|
||||
|
||||
@ -111,6 +111,34 @@ struct LoadContextTelemetry {
|
||||
*/
|
||||
bool sessionMeasurable = false;
|
||||
|
||||
/*!
|
||||
* \brief La source d'avancement est-elle un compteur de VIE (monotone) ou de SESSION ?
|
||||
*
|
||||
* \par Pourquoi cette distinction est la garde, et pas la joignabilité
|
||||
* Un recul ne veut pas dire la même chose selon la source, et **c'est la seule chose que le
|
||||
* moteur sache sans parier sur le plugin** :
|
||||
*
|
||||
* | Source | Un recul signifie | Geste |
|
||||
* |---|---|---|
|
||||
* | session (\c sessionEnergy) | une **nouvelle session** s'est ouverte | recapturer la base |
|
||||
* | vie (\c totalEnergyConsumed) | la valeur n'est **pas fiable** — zéro forgé, compteur perdu | **geler**, ne jamais recapturer |
|
||||
*
|
||||
* **Il n'existe pas de signal de joignabilité sur lequel s'appuyer** : `setupComplete` parle
|
||||
* du setup et non du lien, et `connected` est maintenu par le plugin — la V2C bascule après
|
||||
* trois échecs, un compteur Modbus peut avoir un autre seuil, un compteur générique peut ne
|
||||
* jamais basculer. **Un accumulateur ne doit pas parier sur ce que chaque plugin fait de ses
|
||||
* états en tombant** : mesuré le 2026-08-30, la V2C a forgé un zéro sur \c currentPower et
|
||||
* ses booléens, mais a laissé \c sessionEnergy à sa dernière valeur. La politique dépend du
|
||||
* plugin, ce qui la rend inutilisable comme garde.
|
||||
*
|
||||
* \note Ce que ce champ évite, chiffré : base reprise à 0 pendant l'absence, retour du
|
||||
* compteur avec ses 10 kWh réels, et \c deliveredWh saute à 10 000 en un cycle —
|
||||
* l'obligation se déclare **tenue sans que rien n'ait été livré**. Et `R6` ne s'y oppose
|
||||
* pas : elle n'interdit \c ECO_FLOOR_MET que sous \c unmeasurable. Le chemin **mesuré**
|
||||
* est donc le seul par lequel un faux « tenue » peut sortir.
|
||||
*/
|
||||
bool sessionCumulative = false;
|
||||
|
||||
/*!
|
||||
* \brief Énergie chargée dans la session courante (Wh) — ACCUMULATEUR, pas compteur brut.
|
||||
*
|
||||
|
||||
@ -1014,6 +1014,31 @@ part** (`measuredGaps`). Redater à chaque retour effacerait l'histoire au lieu
|
||||
ne rien dire ferait croire à une continuité que le battement dément. L'écran peut alors écrire
|
||||
« mesuré **par intermittence** depuis 09:14 », qui est la phrase vraie.
|
||||
|
||||
**LM-1012-d — un accumulateur ne parie JAMAIS sur ce qu'un plugin fait de ses états en tombant.**
|
||||
*(2026-09-03, avertissement de l'app intégré AVANT l'écriture.)*
|
||||
|
||||
Un compteur qui disparaît peut **publier 0 au lieu de se taire**. Mesuré sur la V2C le
|
||||
2026-08-30 : `currentPower` 4 396 → 0 et les booléens à `false` à la perte du lien — mais
|
||||
`sessionEnergy` **resté à sa dernière valeur**. **La politique dépend du plugin, ce qui la rend
|
||||
inutilisable comme garde.**
|
||||
|
||||
**Et il n'existe aucun signal de joignabilité sur lequel s'appuyer** : `setupComplete` parle du
|
||||
setup et non du lien ; `connected` est maintenu par le plugin — la V2C bascule après trois échecs,
|
||||
un compteur Modbus peut avoir un autre seuil, un compteur générique peut ne jamais basculer.
|
||||
|
||||
**La garde porte donc sur ce que le moteur sait sans parier : la SÉMANTIQUE du compteur.**
|
||||
|
||||
| Source | Un recul signifie | Geste |
|
||||
|---|---|---|
|
||||
| **session** (`sessionEnergy`) | une nouvelle session s'est ouverte | recapturer la base |
|
||||
| **vie** (`totalEnergyConsumed`) | la valeur n'est pas fiable | **geler**, ne jamais recapturer |
|
||||
|
||||
> **Ce que cela évite, chiffré.** Base reprise à 0 pendant l'absence, retour du compteur avec ses
|
||||
> 10 kWh réels, et `deliveredWh` saute à 10 000 en un cycle : **l'obligation se déclare tenue sans
|
||||
> que rien n'ait été livré**. Et `R6` ne s'y oppose pas — elle n'interdit `ECO_FLOOR_MET` que sous
|
||||
> `unmeasurable`. **Le chemin mesuré est le seul par lequel un faux « tenue » peut sortir**, et
|
||||
> c'est précisément celui qu'ouvre l'alimentation de `sessionWh` depuis un compteur.
|
||||
|
||||
**LM-1013-b — une obligation en ÉNERGIE devient une cible en WATTS par étalement uniforme.**
|
||||
*(§10 étape D, tranché le 2026-08-31.)*
|
||||
|
||||
|
||||
@ -7551,6 +7551,89 @@ void Simulation::testHealthyButUnmeasuredDiffersFromFaulty()
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Un compteur de VIE qui recule ne rebase JAMAIS l'avancement.
|
||||
*
|
||||
* \par Le chemin du faux « tenue », et il est le seul
|
||||
* Un compteur qui disparaît peut publier **0 au lieu de se taire** — mesuré sur la V2C le
|
||||
* 2026-08-30 : `currentPower` 4 396 → 0 et les booléens à `false` à la perte du lien. Si la base
|
||||
* se reprenait sur ce zéro, le retour du compteur avec son cumul réel — 10 kWh sur l'ECS du banc
|
||||
* — ferait sauter `deliveredWh` à 10 000 en un cycle : **l'obligation se déclarerait tenue sans
|
||||
* que rien n'ait été livré**.
|
||||
*
|
||||
* Et `R6` ne s'y oppose pas : elle n'interdit `ECO_FLOOR_MET` que sous `unmeasurable`. Le chemin
|
||||
* **mesuré** est donc le seul par lequel un faux « tenue » peut sortir — c'est-à-dire exactement
|
||||
* celui qu'ouvre l'alimentation de `sessionWh` depuis un compteur.
|
||||
*
|
||||
* \par Pourquoi la garde porte sur la SÉMANTIQUE et non sur la joignabilité
|
||||
* Il n'existe **aucun signal de joignabilité fiable** : `setupComplete` parle du setup et non du
|
||||
* lien, et `connected` est maintenu par le plugin — la V2C bascule après trois échecs, un
|
||||
* compteur générique peut ne jamais basculer. **Un accumulateur ne doit pas parier sur ce que
|
||||
* chaque plugin fait de ses états en tombant** : la même V2C a forgé un zéro sur `currentPower`
|
||||
* et laissé `sessionEnergy` à sa dernière valeur.
|
||||
*
|
||||
* Ce que le moteur sait sans parier : **un compteur de VIE ne recule pas.** Un recul y est une
|
||||
* anomalie ; sur un compteur de SESSION, c'est une session neuve. Deux sources, deux gestes.
|
||||
*/
|
||||
void Simulation::testLifetimeCounterDropNeverRebasesTheProgress()
|
||||
{
|
||||
#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);
|
||||
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 9, 3), QTime(13, 0, 0));
|
||||
|
||||
LoadContext ecs;
|
||||
ecs.id = "ecs-compteur-vie";
|
||||
ecs.needs.minEnergyWhPerDay = 4000;
|
||||
ecs.needs.dailyDeadline = "07:00";
|
||||
ecs.telemetry.sessionMeasurable = true;
|
||||
ecs.telemetry.sessionCumulative = true; // totalEnergyConsumed : monotone par nature
|
||||
ecs.telemetry.sessionWh = 9000; // le compteur en est à 9 kWh de vie
|
||||
|
||||
QCOMPARE(qRound(arb->ecoProgress(ecs, t0).deliveredWh), 0); // base prise à 9000
|
||||
ecs.telemetry.sessionWh = 9500;
|
||||
QCOMPARE(qRound(arb->ecoProgress(ecs, t0).deliveredWh), 500); // 500 Wh livrés
|
||||
|
||||
// LE LIEN TOMBE et le plugin publie ZÉRO au lieu de se taire.
|
||||
ecs.telemetry.sessionWh = 0;
|
||||
const EcoProgress pendant = arb->ecoProgress(ecs, t0);
|
||||
// On GÈLE : la valeur ne vaut rien, donc on ne conclut rien. Pas d'avancement publié, pas
|
||||
// de recapture — le tronçon reste ouvert sur sa dernière valeur saine.
|
||||
QCOMPARE(pendant.regime, QString(ProgressRegime::Unmeasurable));
|
||||
QVERIFY2(!pendant.delivered, "sur une valeur non fiable, on ne publie pas d'avancement");
|
||||
|
||||
// LE COMPTEUR REVIENT avec son cumul réel. C'est ici que le faux « tenue » sortirait si la
|
||||
// base avait été reprise à 0 : 10 000 Wh d'un coup sur une obligation de 4 000.
|
||||
ecs.telemetry.sessionWh = 10000;
|
||||
const EcoProgress apresRetour = arb->ecoProgress(ecs, t0);
|
||||
QCOMPARE(apresRetour.regime, QString(ProgressRegime::Measured));
|
||||
// 10 000 − 9 000 (base intacte) = 1 000 Wh. PAS 10 000.
|
||||
QCOMPARE(qRound(apresRetour.deliveredWh), 1000);
|
||||
QVERIFY2(apresRetour.deliveredWh < ecs.needs.minEnergyWhPerDay,
|
||||
"l'obligation ne doit PAS se déclarer tenue sur un retour de compteur");
|
||||
|
||||
// Et la distinction tient : sur un compteur de SESSION, le même recul EST une session neuve.
|
||||
LoadContext borne;
|
||||
borne.id = "borne-session";
|
||||
borne.needs.minEnergyWhPerDay = 4000;
|
||||
borne.needs.dailyDeadline = "07:00";
|
||||
borne.telemetry.sessionMeasurable = true;
|
||||
borne.telemetry.sessionCumulative = false; // sessionEnergy : remis à zéro par session
|
||||
borne.telemetry.sessionWh = 9000;
|
||||
QCOMPARE(qRound(arb->ecoProgress(borne, t0).deliveredWh), 0);
|
||||
borne.telemetry.sessionWh = 120; // nouvelle session
|
||||
const EcoProgress neuve = arb->ecoProgress(borne, t0);
|
||||
QCOMPARE(neuve.regime, QString(ProgressRegime::Measured)); // pas d'anomalie : on recapture
|
||||
QCOMPARE(qRound(neuve.deliveredWh), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testCountsSumToTarget()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
|
||||
@ -200,6 +200,7 @@ private slots:
|
||||
void testCommandDivergenceIsPublishedNotDiagnosed();
|
||||
void testProgressMeasurabilityIsDeclaredAtConfigTime();
|
||||
void testHealthyButUnmeasuredDiffersFromFaulty();
|
||||
void testLifetimeCounterDropNeverRebasesTheProgress();
|
||||
void testRecreditIsALoanAgainstObedience();
|
||||
void testARefusedStateIsDistinguishableFromAnApplied();
|
||||
void testL4ShedsAPacAndSaysItForcedTheLock();
|
||||
|
||||
@ -19,6 +19,16 @@ Deux motifs, tous deux rencontrés en vrai :
|
||||
`remainingSurplusW`, filé le long de la cascade, avec `allocatedW` accumulé en trois sites
|
||||
distincts. Le motif signale, il ne juge pas — c'est à la lecture de trancher.
|
||||
|
||||
⚠️ CET OUTIL NE TRANCHE PAS, ET NE DOIT JAMAIS TRANCHER.
|
||||
|
||||
Il SIGNALE un motif ; c'est la LECTURE qui décide. Au balayage du 2026-09-03, il a relevé dix
|
||||
identités auto-référentielles : NEUF contrôlaient réellement quelque chose, une seule était vaine.
|
||||
Un outil qui aurait rendu un verdict aurait supprimé neuf contrôles utiles.
|
||||
|
||||
Le prochain qui lira ce fichier sera tenté d'automatiser le verdict. Ne le fais pas : la question
|
||||
« qu'aurait-il attrapé s'il avait échoué ? » demande de savoir d'où viennent les deux membres, et
|
||||
cette connaissance n'est pas dans le texte du test.
|
||||
|
||||
Cet outil ne remplace pas la question, il la pose là où elle se pose souvent.
|
||||
"""
|
||||
import re
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user