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
|
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
|
* 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û.
|
// réelle, et l'écran ne pourrait plus dire ce qui reste dû.
|
||||||
if (!m_ecoMesureDepuis.contains(lc.id))
|
if (!m_ecoMesureDepuis.contains(lc.id))
|
||||||
m_ecoMesureDepuis.insert(lc.id, now);
|
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
|
// 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.
|
// 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,
|
// 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);
|
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
|
void EnergyArbitrator::qualifierRefus(LoadContext &lc, const QDateTime &now) const
|
||||||
{
|
{
|
||||||
const QDateTime depuis = m_divergenceDepuis.value(lc.id);
|
const QDateTime depuis = m_divergenceDepuis.value(lc.id);
|
||||||
@ -1142,6 +1194,7 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
LoadContext lcq = it.value()->toLoadContext(now);
|
LoadContext lcq = it.value()->toLoadContext(now);
|
||||||
|
alimenterAvancement(lcq);
|
||||||
qualifierRefus(lcq, now);
|
qualifierRefus(lcq, now);
|
||||||
ctx.loads.append(lcq);
|
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.lockMinPowerW = 0; // rien d'engagé : rien à geler au budget
|
||||||
lc.telemetry.lockMaxPowerW = -1;
|
lc.telemetry.lockMaxPowerW = -1;
|
||||||
LoadContext lcq = lc;
|
LoadContext lcq = lc;
|
||||||
qualifierRefus(lcq, now);
|
alimenterAvancement(lcq);
|
||||||
|
qualifierRefus(lcq, now);
|
||||||
ctx.loads.append(lcq);
|
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. ---
|
// sg-ready) — une seule table, toutes construites depuis la configuration persistée. ---
|
||||||
for (auto it = m_loadAdapters.constBegin(); it != m_loadAdapters.constEnd(); ++it) {
|
for (auto it = m_loadAdapters.constBegin(); it != m_loadAdapters.constEnd(); ++it) {
|
||||||
LoadContext lcq = it.value()->toLoadContext(now);
|
LoadContext lcq = it.value()->toLoadContext(now);
|
||||||
|
alimenterAvancement(lcq);
|
||||||
qualifierRefus(lcq, now);
|
qualifierRefus(lcq, now);
|
||||||
ctx.loads.append(lcq);
|
ctx.loads.append(lcq);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -705,6 +705,23 @@ private:
|
|||||||
*/
|
*/
|
||||||
void qualifierRefus(LoadContext &lc, const QDateTime &now) const;
|
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É.
|
* \brief Publie l'écart entre ce qu'on a COMMANDÉ et ce qui est MESURÉ.
|
||||||
|
|||||||
@ -111,6 +111,34 @@ struct LoadContextTelemetry {
|
|||||||
*/
|
*/
|
||||||
bool sessionMeasurable = false;
|
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.
|
* \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
|
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.
|
« 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.**
|
**LM-1013-b — une obligation en ÉNERGIE devient une cible en WATTS par étalement uniforme.**
|
||||||
*(§10 étape D, tranché le 2026-08-31.)*
|
*(§10 étape D, tranché le 2026-08-31.)*
|
||||||
|
|
||||||
|
|||||||
@ -7551,6 +7551,89 @@ void Simulation::testHealthyButUnmeasuredDiffersFromFaulty()
|
|||||||
#endif
|
#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()
|
void Simulation::testCountsSumToTarget()
|
||||||
{
|
{
|
||||||
#ifndef ETM_ARBITRATOR
|
#ifndef ETM_ARBITRATOR
|
||||||
|
|||||||
@ -200,6 +200,7 @@ private slots:
|
|||||||
void testCommandDivergenceIsPublishedNotDiagnosed();
|
void testCommandDivergenceIsPublishedNotDiagnosed();
|
||||||
void testProgressMeasurabilityIsDeclaredAtConfigTime();
|
void testProgressMeasurabilityIsDeclaredAtConfigTime();
|
||||||
void testHealthyButUnmeasuredDiffersFromFaulty();
|
void testHealthyButUnmeasuredDiffersFromFaulty();
|
||||||
|
void testLifetimeCounterDropNeverRebasesTheProgress();
|
||||||
void testRecreditIsALoanAgainstObedience();
|
void testRecreditIsALoanAgainstObedience();
|
||||||
void testARefusedStateIsDistinguishableFromAnApplied();
|
void testARefusedStateIsDistinguishableFromAnApplied();
|
||||||
void testL4ShedsAPacAndSaysItForcedTheLock();
|
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
|
`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.
|
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.
|
Cet outil ne remplace pas la question, il la pose là où elle se pose souvent.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user