fix: ancrer les quatre instants stockés à la capture, pas au rendu
DÉFAUT DE CLASSE, trouvé par l'audit demandé après la bascule de fuseau. Quatre instants étaient stockés depuis QDateTime::currentDateTime(), donc en spécification Qt::LocalTime : un tel objet ne garde que des composantes d'HORLOGE MURALE et résout son époque au moment de la CONVERSION, avec le fuseau courant. m_baselinePosee → GetEnergyRatios.measuredSince m_ecoMesureDepuis → progress.measuredSince m_divergenceDepuis → command.since m_lastTelemetryPublish → alimente le précédent Mesuré sur la box le 2026-09-06, bascule Paris→London sans redémarrage : measuredSince 1788645607 → 1788649207, +3600 s. Le champ posé pour rendre une couture lisible était lui-même déplacé par la couture. ET `.toUTC()` AU RENDU NE PROTÉGEAIT DE RIEN — c'est le piège, parce que les quatre rendus l'appelaient déjà et avaient l'air corrects. Démontré par un banc Qt avec bascule de /etc/localtime sous un objet déjà stocké : AVANT stocké-local → 1788755728 stocké-UTC → 1788755728 (Europe/Paris) APRÈS stocké-local → 1788759328 stocké-UTC → 1788755728 (Europe/London) Correctif : `.toUTC()` À LA CAPTURE. periodStartedAt reste construit dans le fuseau courant — c'est une frontière locale, elle DOIT bouger. Les deux champs n'ont pas la même nature et le stockage le reflète maintenant. TEST avec son oracle : l'ancrage tient sous re-rendu, la contre-épreuve reproduit la dérive de +3600 s, et une quatrième assertion dit ce qui compte — le même décalage est un DÉFAUT sur l'instant de pose et le COMPORTEMENT ATTENDU sur la frontière. Le nombre ne dit pas lequel on regarde ; seule la nature du champ le dit. SIXIÈME FORME D'HOMOGÉNÉITÉ (AGENTS.md) : ce n'est plus le mock, c'est L'ENVIRONNEMENT DE VÉRIFICATION. Box et téléphone en Europe/Paris pendant toute la campagne : à décalage nul, un instant rendu en UTC, en heure box ou en heure téléphone donne le MÊME TEXTE. Aucun défaut de conversion ne pouvait se voir, et rien dans le code ne pouvait le signaler — la prémisse fausse n'est pas dans le banc, elle est dans la coïncidence de deux réglages extérieurs. D'où le geste : basculer le fuseau de temps en temps est un RÉVÉLATEUR, pas seulement une manipulation de capture. Quinze minutes en Europe/London ont dénoncé deux défauts, dont un qui avait traversé toute la campagne. Suites : loadmodel 25/25 (+1), simulation 67/67, charging 48/48, spotmarket 32/32. NON DÉPLOYÉ.
This commit is contained in:
parent
d6cc52bd43
commit
b129e97346
@ -584,7 +584,9 @@ EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc, const QDateTime
|
||||
// avant : crédible et faux. Rebaser `targetWh` serait pire — on perdrait l'obligation
|
||||
// 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);
|
||||
// Ancré en UTC à la CAPTURE : un QDateTime local résout son époque à la
|
||||
// conversion, donc il dérive au changement de fuseau (cf. energyratioscalculator).
|
||||
m_ecoMesureDepuis.insert(lc.id, now.toUTC());
|
||||
} 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 :
|
||||
@ -1836,7 +1838,7 @@ void EnergyArbitrator::attachCommandDivergence(QVariantMap &entry, const QString
|
||||
} else if (!m_divergenceDepuis.contains(loadId)) {
|
||||
const QDateTime debut = m_lastTelemetryPublish.isValid()
|
||||
? m_lastTelemetryPublish : QDateTime::currentDateTime();
|
||||
m_divergenceDepuis.insert(loadId, debut);
|
||||
m_divergenceDepuis.insert(loadId, debut.toUTC()); // ancré à la capture
|
||||
qCInfo(dcNymeaEnergy()) << "[Divergence] DÉBUT" << loadId
|
||||
<< "— attendu" << attenduW << "W, mesuré" << mesureW << "W,"
|
||||
<< "écart" << (mesureW - attenduW) << "W";
|
||||
@ -1996,7 +1998,7 @@ void EnergyArbitrator::publishTelemetry(bool force)
|
||||
return;
|
||||
|
||||
m_publishedTelemetry = m_telemetry;
|
||||
m_lastTelemetryPublish = QDateTime::currentDateTime();
|
||||
m_lastTelemetryPublish = QDateTime::currentDateTime().toUTC(); // ancré à la capture
|
||||
// Le battement est un PLANCHER de cadence, pas un ajout : toute notification relance son
|
||||
// décompte. Sur une box qui cycle chaque minute, le total reste donc d'environ UNE
|
||||
// notification par minute — ce que la cible RPi Zero 2W tient sans y penser. Porter cette
|
||||
|
||||
@ -66,7 +66,17 @@ EnergyRatiosCalculator::Ratios EnergyRatiosCalculator::compute(double totalProdu
|
||||
m_baseAcquisition = totalAcquisition;
|
||||
// L'instant de pose, distinct de la frontière de journée : un redémarrage de nymead
|
||||
// repose la baseline en plein jour, et les ratios ne couvrent alors que depuis là.
|
||||
m_baselinePosee = now;
|
||||
//
|
||||
// ANCRÉ EN UTC À LA CAPTURE, et c'est indispensable. Un QDateTime en spécification
|
||||
// `Qt::LocalTime` ne garde que des composantes d'HORLOGE MURALE : son époque se résout
|
||||
// au moment de la CONVERSION, avec le fuseau courant. Stocké tel quel, cet instant
|
||||
// se déplaçait d'une heure au changement de fuseau — mesuré le 2026-09-06, bascule
|
||||
// Paris→London : 1788645607 → 1788649207. Le champ posé pour rendre une couture
|
||||
// lisible était lui-même déplacé par la couture.
|
||||
//
|
||||
// Un `.toUTC()` AU RENDU n'y aurait rien changé : il interprète les composantes murales
|
||||
// avec le fuseau du moment. Seul l'ancrage à la capture tient.
|
||||
m_baselinePosee = now.toUTC();
|
||||
}
|
||||
|
||||
const double dProduction = totalProduction - m_baseProduction;
|
||||
|
||||
@ -701,6 +701,47 @@ void TestLoadModel::testEcoUnmeasurableBuysAtAConstantRate()
|
||||
QCOMPARE(qRound(inconnue(5, 0).remainingWh), 4000);
|
||||
}
|
||||
|
||||
void TestLoadModel::testStoredInstantsSurviveATimeZoneChange()
|
||||
{
|
||||
// Un instant ABSOLU ne bouge pas quand l'installation change de fuseau ; une FRONTIÈRE
|
||||
// locale, si. Les deux vivent dans la même charge utile et se ressemblent — c'est par là
|
||||
// que le défaut est passé (mesuré le 2026-09-06, bascule Paris→London sans redémarrage :
|
||||
// measuredSince a bougé de +3600 s alors qu'il datait une pose déjà faite).
|
||||
const QTimeZone paris("Europe/Paris"), londres("Europe/London");
|
||||
QVERIFY2(paris.isValid() && londres.isValid(), "sans base de fuseaux ce test ne prouve rien");
|
||||
|
||||
// L'instant de pose, tel que le code le capture désormais : ancré à la capture.
|
||||
const QDateTime pose = QDateTime(QDate(2026, 9, 6), QTime(15, 36, 42), paris).toUTC();
|
||||
const qint64 epoquePose = pose.toSecsSinceEpoch();
|
||||
|
||||
// 1. L'ANCRAGE TIENT : re-rendre dans un autre fuseau ne déplace pas l'instant.
|
||||
QCOMPARE(pose.toTimeZone(londres).toSecsSinceEpoch(), epoquePose);
|
||||
QCOMPARE(pose.toUTC().toSecsSinceEpoch(), epoquePose);
|
||||
|
||||
// 2. CE QUI ARRIVAIT SANS L'ANCRAGE, et c'est la contre-épreuve : un QDateTime en
|
||||
// spécification LocalTime ne garde que des composantes d'horloge murale, donc son
|
||||
// époque se résout AVEC LE FUSEAU COURANT. Reproduit ici en construisant la même
|
||||
// lecture murale dans les deux fuseaux — c'est exactement ce que fait Qt quand
|
||||
// /etc/localtime bascule sous un objet déjà stocké.
|
||||
const QDateTime muraleParis(QDate(2026, 9, 6), QTime(15, 36, 42), paris);
|
||||
const QDateTime muraleLondres(QDate(2026, 9, 6), QTime(15, 36, 42), londres);
|
||||
QCOMPARE(muraleLondres.toSecsSinceEpoch() - muraleParis.toSecsSinceEpoch(), qint64(3600));
|
||||
QVERIFY2(muraleLondres.toSecsSinceEpoch() != epoquePose,
|
||||
"sans ancrage, la même lecture murale rend une autre époque — c'est le défaut");
|
||||
|
||||
// 3. LA FRONTIÈRE, ELLE, DOIT BOUGER. Minuit local n'est pas un instant absolu : c'est une
|
||||
// propriété du fuseau, et la publier fixe serait l'autre erreur, symétrique.
|
||||
const QDateTime minuitParis(QDate(2026, 9, 6), QTime(0, 0), paris);
|
||||
const QDateTime minuitLondres(QDate(2026, 9, 6), QTime(0, 0), londres);
|
||||
QCOMPARE(minuitLondres.toSecsSinceEpoch() - minuitParis.toSecsSinceEpoch(), qint64(3600));
|
||||
|
||||
// 4. Et c'est là toute la distinction, en une assertion : le même décalage de +3600 s est
|
||||
// un DÉFAUT sur l'instant de pose et le COMPORTEMENT ATTENDU sur la frontière. Le nombre
|
||||
// ne dit pas lequel des deux on regarde — seule la nature du champ le dit.
|
||||
QCOMPARE(muraleLondres.toSecsSinceEpoch() - muraleParis.toSecsSinceEpoch(),
|
||||
minuitLondres.toSecsSinceEpoch() - minuitParis.toSecsSinceEpoch());
|
||||
}
|
||||
|
||||
void TestLoadModel::testReportingDayBoundaryIsPublishedNotDeducible()
|
||||
{
|
||||
// \rule{LM-1005-b-iv} — la frontière de la période de reporting se PUBLIE. Ce test dit
|
||||
|
||||
@ -81,6 +81,7 @@ private slots:
|
||||
//! lequel. Aucun plafond déclaré ≠ marge nulle. Départage stable à marge égale.
|
||||
void testEcoSpreadHardensAndSaysSo();
|
||||
void testEcoUnmeasurableBuysAtAConstantRate();
|
||||
void testStoredInstantsSurviveATimeZoneChange();
|
||||
void testReportingDayBoundaryIsPublishedNotDeducible();
|
||||
void testDrawMarginComparesHeadroomsNotCaps();
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user