fix: l'avancement survit au battement du régime dans une même période
Sur measured → unmeasurable → measured dans la même fenêtre, deliveredWh repartait de zéro : la base était retirée puis reprise à la valeur courante, donc l'énergie livrée pendant les tronçons mesurés antérieurs disparaissait. L'écran sous-estimait, et — ce qui coûte — la passe éco rachetait le déjà livré. Ce n'est pas théorique : c'est le motif d'une V2C dont le lien Modbus bat. Chaque perte et retour fait basculer le régime deux fois, et chaque aller-retour effaçait ce qui précède. Contre-épreuve : tronçon jeté, l'avancement retombe de 1500 Wh à 0. On accumule par PÉRIODE, pas par tronçon — le tronçon est clos, pas abandonné. Et la clôture se fait sur la dernière valeur lue TANT QUE la mesure existait, jamais sur celle du cycle où le régime tombe : à cet instant le Thing ne publie plus, et s'en servir fabriquerait une énergie. Deux tranchages, épinglés. Le cumul de période repart de zéro à la bascule de période, et les trous avec lui — ils comptaient les interruptions de la période close. Et measuredSince garde le PREMIER instant mesuré, l' intermittence se disant à part : redater à chaque retour effacerait l'histoire au lieu de la raconter, ne rien dire ferait croire à une continuité que le battement dément. LM-1014-c : un cumul se teste aussi de part et d'autre d'un CHANGEMENT DE RÉGIME, pas seulement de période. LM-1014-b ne disait rien de la bascule de régime à l'intérieur d'une période — c'est exactement là que le défaut vivait. Le test a échoué deux fois avant d'être juste, et la seconde fois c'était le test : mon horodatage « après échéance » visait la même échéance que le premier, donc rien ne basculait. Suite : simulation 62/62, 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
cdd3bc88f3
commit
fa13420eec
@ -698,6 +698,14 @@ C'est un **cumul sur la fenêtre d'obligation** : remis à zéro à la bascule d
|
||||
> `measuredSince` prend la seconde sortie : l'écran dit **« mesuré depuis 09:14 »**, et
|
||||
> `purchasedWh` couvre la période d'avant. **Les deux se répondent**, et ensemble ils disent la
|
||||
> vérité complète. Le champ est **absent** tant que rien n'a jamais été mesuré dans la période.
|
||||
>
|
||||
> **`measuredGaps` — présent seulement s'il y a eu des trous.** `measuredSince` date le PREMIER
|
||||
> instant mesuré de la période ; sur une borne dont le lien bat, la continuité qu'il suggère est
|
||||
> fausse. Ce compteur autorise la phrase honnête : **« mesuré par intermittence depuis 09:14 »**.
|
||||
>
|
||||
> **`deliveredWh` survit aux trous** : les tronçons mesurés d'une même période s'**additionnent**
|
||||
> (`LM-1014-c`). Sans cela, chaque retour de lien effaçait ce qui précède — l'écran
|
||||
> sous-estimait, et la passe éco **rachetait le déjà livré**.
|
||||
|
||||
### `progressMeasurable` / `progressUnmeasurableCause` — dans `GetLoadConfig`, pas dans la télémétrie
|
||||
|
||||
|
||||
@ -1,3 +1,31 @@
|
||||
powersync-energy-plugin-nymea (1.15.2+etm49) trixie; urgency=medium
|
||||
|
||||
* CORRECTIF — sur `measured → unmeasurable → measured` DANS LA MÊME PÉRIODE, `deliveredWh`
|
||||
repartait de zéro. La base était retirée puis reprise à la valeur courante de `sessionWh`,
|
||||
donc l'énergie livrée pendant les tronçons mesurés antérieurs DISPARAISSAIT. Deux
|
||||
conséquences, et la seconde coûte : l'écran sous-estime, et la passe éco RACHÈTE ce qui a
|
||||
déjà été livré.
|
||||
* PAS THÉORIQUE : c'est le motif d'une V2C dont le lien Modbus bat — chaque perte et retour
|
||||
fait basculer le régime deux fois, et chaque aller-retour effaçait ce qui précède.
|
||||
Contre-épreuve : tronçon jeté, l'avancement retombe de 1500 Wh à 0.
|
||||
* On accumule désormais par PÉRIODE, pas par tronçon. Et la clôture se fait sur la DERNIÈRE
|
||||
valeur lue tant que la mesure existait, jamais sur celle du cycle où le régime tombe : à cet
|
||||
instant le Thing ne publie plus, et s'en servir fabriquerait une énergie.
|
||||
* TRANCHÉ — le cumul de période repart de zéro à la bascule de période, et les trous avec lui.
|
||||
* TRANCHÉ — `measuredSince` garde le PREMIER instant mesuré, et l'intermittence se dit à part
|
||||
(`measuredGaps`, publié seulement s'il y en a). Redater à chaque retour effacerait l'histoire
|
||||
au lieu de la raconter ; ne rien dire ferait croire à une continuité que le battement dément.
|
||||
L'écran peut écrire « mesuré PAR INTERMITTENCE depuis 09:14 », qui est la phrase vraie.
|
||||
* LM-1014-c — un cumul se teste aussi de part et d'autre d'un CHANGEMENT DE RÉGIME, pas
|
||||
seulement de période. LM-1014-b disait quoi faire à la bascule de période et ne disait rien
|
||||
de la bascule de régime à l'intérieur d'une période : c'est là que le défaut vivait.
|
||||
* Le test a échoué deux fois avant d'être juste, et la seconde était le test : mon horodatage
|
||||
« après échéance » visait la MÊME échéance que le premier, donc rien ne basculait.
|
||||
* Suite : simulation 62/62, loadmodel 22/22, charging 48/48 identique à la référence,
|
||||
spotmarket 32/32, doxygen 0 avertissement.
|
||||
|
||||
-- Patrick Schurig <etm.schurig@gmail.com> Wed, 02 Sep 2026 11:00:00 +0200
|
||||
|
||||
powersync-energy-plugin-nymea (1.15.2+etm48) trixie; urgency=medium
|
||||
|
||||
* MOCK — classe `dryRelay` : un relais SEC, qui commute et ne mesure pas. Elle ouvre le cas que
|
||||
|
||||
@ -532,6 +532,12 @@ EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc, const QDateTime
|
||||
m_ecoAchatWh.remove(lc.id);
|
||||
m_ecoDernierAchat.remove(lc.id);
|
||||
m_ecoMesureDepuis.remove(lc.id);
|
||||
// Le cumul de période repart de ZÉRO, comme tout ce qui est rapporté à la période
|
||||
// (\rule{LM-1014-b}) — et les trous avec lui : ils comptaient les interruptions de la
|
||||
// période close, pas de la neuve.
|
||||
m_ecoLivreWh.remove(lc.id);
|
||||
m_ecoDernierSessionWh.remove(lc.id);
|
||||
m_ecoTrous.remove(lc.id);
|
||||
}
|
||||
|
||||
// C1 de LM-1009 — la borne ne compte pas : l'obligation n'est PAS mesurable, et ça se dit.
|
||||
@ -545,8 +551,24 @@ EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc, const QDateTime
|
||||
|
||||
if (!lc.telemetry.sessionMeasurable) {
|
||||
p.regime = QString::fromLatin1(ProgressRegime::Unmeasurable);
|
||||
m_ecoBaseWh.remove(lc.id);
|
||||
m_ecoMesureDepuis.remove(lc.id); // on ne mesure plus : la date n'a plus d'objet
|
||||
// CLÔTURE DU TRONÇON, pas abandon. Un aller-retour de régime dans la même période — le
|
||||
// motif d'un lien Modbus qui bat — jetait tout ce qui précède : la base était reprise à
|
||||
// la valeur courante, et l'avancement repartait de zéro. Deux conséquences, et la
|
||||
// seconde coûte : l'écran sous-estime, et la passe éco RACHÈTE le déjà livré.
|
||||
//
|
||||
// On additionne au cumul de période, en partant de la DERNIÈRE valeur lue tant que la
|
||||
// mesure existait : la valeur du cycle courant ne vaut plus rien puisque le Thing ne
|
||||
// publie plus, et s'en servir fabriquerait une énergie.
|
||||
if (m_ecoBaseWh.contains(lc.id)) {
|
||||
const double fin = m_ecoDernierSessionWh.value(lc.id, m_ecoBaseWh.value(lc.id));
|
||||
m_ecoLivreWh[lc.id] += qMax(0.0, fin - m_ecoBaseWh.value(lc.id));
|
||||
m_ecoTrous[lc.id] += 1;
|
||||
m_ecoBaseWh.remove(lc.id);
|
||||
}
|
||||
// `m_ecoMesureDepuis` est CONSERVÉ : il date le PREMIER instant mesuré de la période, et
|
||||
// les trous sont dits séparément. Le remettre à zéro ferait croire à une mesure continue
|
||||
// qui commencerait au retour du lien.
|
||||
p.purchasedWh = m_ecoAchatWh.value(lc.id, 0.0);
|
||||
return p;
|
||||
}
|
||||
|
||||
@ -575,12 +597,18 @@ EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc, const QDateTime
|
||||
m_ecoBaseWh.insert(lc.id, sessionWh);
|
||||
}
|
||||
|
||||
m_ecoDernierSessionWh.insert(lc.id, sessionWh); // ce qui clôturera le tronçon s'il tombe
|
||||
|
||||
p.regime = QString::fromLatin1(ProgressRegime::Measured);
|
||||
p.delivered = true;
|
||||
p.measuredSince = m_ecoMesureDepuis.value(lc.id);
|
||||
p.measuredGaps = m_ecoTrous.value(lc.id, 0);
|
||||
// Un recul INFIME laisse la base au-dessus de la valeur lue ; l'écrêtage évite un
|
||||
// avancement négatif sans masquer un vrai recul, qui a déjà provoqué la recapture.
|
||||
p.deliveredWh = qMax(0.0, sessionWh - m_ecoBaseWh.value(lc.id));
|
||||
// Le livré de la période = les tronçons CLOS + celui en cours. Sans le premier terme, un
|
||||
// lien qui bat efface son propre historique à chaque retour.
|
||||
p.deliveredWh = m_ecoLivreWh.value(lc.id, 0.0)
|
||||
+ qMax(0.0, sessionWh - m_ecoBaseWh.value(lc.id));
|
||||
return p;
|
||||
}
|
||||
|
||||
@ -1619,6 +1647,11 @@ void EnergyArbitrator::buildTelemetry(const Plan &plan, const Slot &slot, const
|
||||
if (n.progress.measuredSince.isValid())
|
||||
progress.insert(QStringLiteral("measuredSince"),
|
||||
n.progress.measuredSince.toUTC().toString(Qt::ISODate));
|
||||
// L'INTERMITTENCE, publiée seulement si elle a eu lieu. `measuredSince` date le
|
||||
// PREMIER instant mesuré ; sans ce compteur, il suggérerait une continuité que
|
||||
// le battement d'un lien Modbus dément. Absent = mesure continue.
|
||||
if (n.progress.measuredGaps > 0)
|
||||
progress.insert(QStringLiteral("measuredGaps"), n.progress.measuredGaps);
|
||||
niveau.insert(QStringLiteral("progress"), progress);
|
||||
}
|
||||
niveaux.append(niveau);
|
||||
|
||||
@ -801,6 +801,18 @@ private:
|
||||
mutable QHash<QString, QDateTime> m_ecoDernierAchat;
|
||||
//! loadId → instant où l'avancement est devenu MESURABLE dans la période courante.
|
||||
mutable QHash<QString, QDateTime> m_ecoMesureDepuis;
|
||||
//! loadId → énergie livrée (Wh) sur les TRONÇONS MESURÉS DÉJÀ CLOS de la période courante.
|
||||
//! Sans ce cumul, un aller-retour de régime — un lien Modbus qui bat — effacerait tout ce
|
||||
//! qui précède : l'écran sous-estimerait, et la passe éco RACHÈTERAIT le déjà livré.
|
||||
mutable QHash<QString, double> m_ecoLivreWh;
|
||||
//! loadId → dernière valeur de \c sessionWh lue TANT QUE la mesure était disponible. C'est
|
||||
//! elle qui clôt le tronçon : au moment où le régime tombe, la valeur courante ne vaut plus
|
||||
//! rien (le Thing ne publie plus), et l'utiliser fabriquerait une énergie.
|
||||
mutable QHash<QString, double> m_ecoDernierSessionWh;
|
||||
//! loadId → nombre d'interruptions de mesure dans la période. Publié quand non nul : c'est
|
||||
//! ce qui autorise l'écran à dire « mesuré PAR INTERMITTENCE depuis 09:14 » plutôt qu'une
|
||||
//! continuité qui n'a pas eu lieu.
|
||||
mutable QHash<QString, int> m_ecoTrous;
|
||||
ThingManager *m_tm = nullptr; //!< ThingManager (pour construire les adaptateurs config).
|
||||
LoadConfigStore *m_loadConfigStore = nullptr; //!< Store config charge pilotée (non adopté).
|
||||
|
||||
|
||||
@ -76,6 +76,17 @@ struct EcoProgress {
|
||||
*/
|
||||
QDateTime measuredSince;
|
||||
|
||||
/*!
|
||||
* \brief Nombre d'INTERRUPTIONS de mesure dans la période. 0 = mesure continue.
|
||||
*
|
||||
* \c measuredSince date le PREMIER instant mesuré de la période ; avec des trous, la
|
||||
* continuité qu'il suggère serait fausse. Ce compteur permet à l'écran de dire « mesuré
|
||||
* **par intermittence** depuis 09:14 » — la phrase honnête — plutôt qu'une continuité qui
|
||||
* n'a pas eu lieu. Sans lui, la seule sortie serait de redater à chaque retour, ce qui
|
||||
* effacerait l'histoire au lieu de la raconter.
|
||||
*/
|
||||
int measuredGaps = 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.
|
||||
|
||||
@ -981,6 +981,39 @@ Le corollaire de test s'écrit tout seul : **un cumul se teste de part et d'autr
|
||||
jamais seulement en régime établi. Les deux défauts ci-dessus ont survécu à des suites vertes
|
||||
parce qu'aucun scénario ne traversait la frontière.
|
||||
|
||||
**LM-1014-c — un cumul se teste aussi de part et d'autre d'un CHANGEMENT DE RÉGIME, pas
|
||||
seulement de période.** *(Corollaire de `LM-1014-b`, trouvé par lecture le 2026-09-02.)*
|
||||
|
||||
`LM-1014-b` dit ce qu'un cumul devient à la bascule de **période**. Elle ne disait rien de la
|
||||
bascule de **régime à l'intérieur d'une période** — et c'est là que le défaut vivait.
|
||||
|
||||
Sur `measured → unmeasurable → measured` dans la même fenêtre, la base d'avancement était retirée
|
||||
puis **reprise à la valeur courante** : l'énergie livrée pendant les tronçons mesurés antérieurs
|
||||
disparaissait. Deux conséquences, et la seconde coûte :
|
||||
|
||||
1. l'écran **sous-estime** ;
|
||||
2. la passe éco **rachète ce qui a déjà été livré**.
|
||||
|
||||
**Ce n'est pas théorique** : c'est le motif exact d'une V2C dont le lien Modbus bat — chaque perte
|
||||
et retour fait basculer le régime **deux fois**, et chaque aller-retour effaçait ce qui précède.
|
||||
|
||||
**La correction : accumuler par PÉRIODE, pas par tronçon.** En quittant `measured`, on ajoute
|
||||
`dernierSessionWh − base` à un cumul de période au lieu de le jeter. Le tronçon est **clos**, pas
|
||||
abandonné.
|
||||
|
||||
> **Et il faut clôturer sur la DERNIÈRE valeur lue tant que la mesure existait**, jamais sur celle
|
||||
> du cycle où le régime tombe : à cet instant le Thing ne publie plus, et s'en servir fabriquerait
|
||||
> une énergie.
|
||||
|
||||
**LM-1014-c-i — le cumul de période repart de zéro à la bascule de période**, comme tout ce qui
|
||||
lui est rapporté, et **les trous avec lui** : ils comptaient les interruptions de la période
|
||||
close.
|
||||
|
||||
**LM-1014-c-ii — `measuredSince` garde le PREMIER instant mesuré, et l'intermittence se dit à
|
||||
part** (`measuredGaps`). Redater à chaque retour effacerait l'histoire au lieu de la raconter ;
|
||||
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-1013-b — une obligation en ÉNERGIE devient une cible en WATTS par étalement uniforme.**
|
||||
*(§10 étape D, tranché le 2026-08-31.)*
|
||||
|
||||
|
||||
@ -6795,6 +6795,52 @@ void Simulation::testEcoProgressBaseAndRegime()
|
||||
quotidienne.telemetry.sessionWh = 3300;
|
||||
QCOMPARE(qRound(arb->ecoProgress(quotidienne, apres).deliveredWh), 500);
|
||||
|
||||
// 8. BASCULE DE RÉGIME DANS LA MÊME PÉRIODE — measured → unmeasurable → measured.
|
||||
// C'est le motif d'une V2C dont le lien Modbus bat : chaque perte et retour fait
|
||||
// basculer le régime DEUX fois. L'énergie livrée pendant les tronçons mesurés
|
||||
// antérieurs ne doit pas disparaître — sinon l'écran sous-estime, et surtout la passe
|
||||
// éco RACHÈTE ce qui a déjà été livré.
|
||||
LoadContext bat;
|
||||
bat.id = "borne-intermittente";
|
||||
bat.needs.minEnergyWhPerDay = 4000;
|
||||
bat.needs.dailyDeadline = "07:00";
|
||||
|
||||
bat.telemetry.sessionMeasurable = true;
|
||||
bat.telemetry.sessionWh = 1000;
|
||||
QCOMPARE(qRound(arb->ecoProgress(bat, t0).deliveredWh), 0); // base prise ici
|
||||
bat.telemetry.sessionWh = 2500;
|
||||
QCOMPARE(qRound(arb->ecoProgress(bat, t0).deliveredWh), 1500); // 1500 Wh livrés
|
||||
|
||||
// Le lien tombe : plus rien ne mesure.
|
||||
bat.telemetry.sessionMeasurable = false;
|
||||
QCOMPARE(arb->ecoProgress(bat, t0).regime, QString(ProgressRegime::Unmeasurable));
|
||||
|
||||
// Le lien revient, dans la MÊME période. Les 1500 Wh déjà livrés doivent SURVIVRE.
|
||||
bat.telemetry.sessionMeasurable = true;
|
||||
bat.telemetry.sessionWh = 2500;
|
||||
QCOMPARE(qRound(arb->ecoProgress(bat, t0).deliveredWh), 1500);
|
||||
bat.telemetry.sessionWh = 3000;
|
||||
const EcoProgress reprise = arb->ecoProgress(bat, t0);
|
||||
QCOMPARE(qRound(reprise.deliveredWh), 2000); // 1500 (tronçon clos) + 500 (en cours)
|
||||
|
||||
// TRANCHÉ — `measuredSince` garde le PREMIER instant mesuré, et l'intermittence se dit à
|
||||
// part. Redater à chaque retour effacerait l'histoire au lieu de la raconter ; 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.
|
||||
QCOMPARE(reprise.measuredSince, t0);
|
||||
QCOMPARE(reprise.measuredGaps, 1);
|
||||
|
||||
// TRANCHÉ — à la BASCULE DE PÉRIODE, le cumul repart de zéro comme tout ce qui est rapporté
|
||||
// à la période (LM-1014-b), et les trous avec lui : ils comptaient les interruptions de la
|
||||
// période close, pas de la neuve.
|
||||
// Au-delà de l'échéance visée depuis t0 (2026-08-31 13:00 → 2026-09-01 07:00) : c'est ce
|
||||
// franchissement qui ouvre une période neuve. Un instant qui viserait la MÊME échéance ne
|
||||
// ferait rien basculer — première version de ce test, qui échouait pour cette raison.
|
||||
const QDateTime apresEcheance = utcDateTime(QDate(2026, 9, 1), QTime(8, 0, 0));
|
||||
const EcoProgress neuve = arb->ecoProgress(bat, apresEcheance);
|
||||
QCOMPARE(qRound(neuve.deliveredWh), 0);
|
||||
QCOMPARE(neuve.measuredGaps, 0);
|
||||
|
||||
// 6. L'obligation retirée LIBÈRE la base : une nouvelle obligation repart d'un repère neuf,
|
||||
// et n'hérite pas de l'avancement d'une obligation qui n'existe plus.
|
||||
LoadContext retiree = mesuree;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user