feat: journaliser la divergence, et publier la frontière de période de reporting
DIVERGENCE JOURNALISÉE. `divergent` et `since` ne vivaient que dans la charge utile de télémétrie et dans un QHash en mémoire — donc nulle part. Rien ne comptait les épisodes : chaque redémarrage de nymead effaçait l'état, et le hash est PURGÉ dès que l'écart cesse, si bien qu'une divergence qui s'ouvre et se referme entre deux lectures ne laissait aucune trace. Un observateur externe échantillonnant en continu aurait raté exactement les épisodes courts, qui sont probablement les plus nombreux. Conséquence : le critère « attendre que les traces parlent » attendait indéfiniment. Le silence du journal était garanti par construction, pas par l'absence du phénomène — l'instrument dont le silence ressemble à un résultat. Deux `qCInfo`, aux deux transitions, avec la durée à la fermeture. Le journal de .75 est persistant depuis le 2026-08-13 (200 Mo, un mois) : le comptage devient trivial, et l'étape 4 pourra se décider sur des faits. FRONTIÈRE DE PÉRIODE PUBLIÉE (LM-1005-b-iv). `System.GetTime` donne un NOM de fuseau ; un décalage ne s'en déduit pas sans base de fuseaux, et embarquer une base pour recalculer une frontière que le moteur connaît déjà resterait faux les jours de changement d'heure, où la journée fait 23 ou 25 heures. Trois clients la recalculeraient de trois façons, et la seule qui compte est celle qui a servi au calcul. Même règle que `counts`. C'est aussi la parade à la troisième nature d'heure murale : la période de reporting suit l'installation, correctement, mais repose sur la prémisse que le fuseau de la box SOIT celui de l'installation — et c'était le seul des trois cas où personne ne vérifiait. `timeZone` rend la vérification possible. DEUX CHAMPS, PAS UN (LM-1005-b-v), et c'est un ajout à la demande. La baseline se repose à chaque redémarrage de nymead et à chaque recul de compteur, donc EN PLEIN JOUR. Un jour où nymead a redémarré à 14 h, les ratios ne couvrent pas la journée : ils couvrent depuis 14 h. Publier la seule frontière ferait écrire « aujourd'hui » sur des chiffres qui ne parlent que de l'après-midi — un silence de la même famille que celui qu'on ferme. `measuredSince` dit la portée réelle ; quand les deux coïncident, le cas normal, il n'y a rien à dire de plus, et c'est ce qui rend l'écart lisible le jour où il apparaît. Même paire, même raison que `progress.measuredSince` / `targetWh`. Les trois champs sont TOUJOURS présents, même quand les deux taux sont omis : « la période a commencé à minuit et rien n'est encore mesurable » est une phrase vraie et utile, là où une réponse vide ne se distingue pas d'une panne. INVARIANT 11 au passage : le schéma du Get et celui du Changed étaient déclarés DEUX fois. Ajouter trois champs à deux endroits est exactement la manière dont ils divergent — d'où `energyRatiosSchema()`, déclaré une fois et servi aux deux, comme `loadTelemetrySchema()`. TEST vu mordre : remplacer `measuredSince` par la frontière — l'implémentation naïve que la demande aurait pu produire — fait échouer l'assertion sur la repose en plein jour (attendu 10:00, obtenu 00:00). Suites : simulation 66/66, loadmodel 23/23, charging 48/48, spotmarket 32/32.
This commit is contained in:
parent
be556ef207
commit
ae5e8639c4
48
INTERFACE.md
48
INTERFACE.md
@ -1296,6 +1296,54 @@ charge utile, et **seulement la sienne**. Toutes les clés internes sont optionn
|
||||
|
||||
---
|
||||
|
||||
### Taux d'énergie — `GetEnergyRatios` / `EnergyRatiosChanged`
|
||||
|
||||
| champ | présence | sens |
|
||||
|---|---|---|
|
||||
| `selfConsumptionRate` | **omis** si n/a | part de la production consommée sur place, % dans [0, 100] |
|
||||
| `autonomyRate` | **omis** si n/a | part de la consommation couverte sans soutirage, % |
|
||||
| `periodStartedAt` | **toujours** | epoch (s) du **minuit local** qui ouvre la période de reporting |
|
||||
| `measuredSince` | **toujours** | epoch (s) où la **baseline courante a été posée** |
|
||||
| `timeZone` | **toujours** | identifiant IANA du fuseau où la frontière a été construite |
|
||||
|
||||
Les trois derniers sont présents **même quand les deux taux sont omis** : « la période a commencé
|
||||
à minuit et rien n'est encore mesurable » est une phrase vraie et utile, là où une réponse vide ne
|
||||
se distingue pas d'une panne.
|
||||
|
||||
> #### Pourquoi la frontière est PUBLIÉE et non recalculée
|
||||
>
|
||||
> `System.GetTime` publie un **nom IANA**. Un décalage ne s'en déduit pas sans base de fuseaux, et
|
||||
> embarquer une base pour recalculer une frontière que le moteur connaît déjà serait le mauvais
|
||||
> geste — le recalcul resterait **faux les jours de changement d'heure**, où la journée fait 23 ou
|
||||
> 25 heures. Trois clients la recalculeraient de trois façons ; la seule qui compte est celle qui
|
||||
> a servi au calcul. **Même règle que `counts`.**
|
||||
|
||||
> #### `periodStartedAt` ≠ `measuredSince`, et l'écart est la seule chose qui empêche les ratios de mentir
|
||||
>
|
||||
> La baseline se repose à **chaque redémarrage de nymead** et à chaque recul de compteur — donc en
|
||||
> plein jour. Un jour où nymead a redémarré à 14 h, ces ratios ne couvrent pas la journée : ils
|
||||
> couvrent **depuis 14 h**.
|
||||
>
|
||||
> Avec la seule frontière, l'écran écrirait « aujourd'hui » sur des chiffres qui ne parlent que de
|
||||
> l'après-midi. Avec la paire, il peut écrire la phrase vraie : **« depuis 14:00 »**. Quand les
|
||||
> deux coïncident — le cas normal — il n'y a rien à dire de plus, et c'est ce qui rend l'écart
|
||||
> lisible le jour où il apparaît.
|
||||
>
|
||||
> Même paire, même raison que `progress.measuredSince` / `targetWh` de l'avancement éco.
|
||||
|
||||
> #### Ce que `timeZone` rend possible, et qui n'existait pas
|
||||
>
|
||||
> La période de reporting est la **troisième nature d'heure murale** du système, après l'habitude
|
||||
> du client et le calendrier réglementé (`LM-1005-b`). Elle suit l'installation, ce qui est
|
||||
> correct — **mais seulement si le fuseau de la box est celui de l'installation.** Relevé le
|
||||
> 2026-09-04 : `.75` tourne en `Europe/London`, donc son bilan journalier se remet à zéro à
|
||||
> **01:00 heure française**, et rien nulle part ne le disait.
|
||||
>
|
||||
> Publier le fuseau est ce qui rend cette vérification **possible** — c'est le seul des trois cas
|
||||
> où personne ne contrôlait quoi que ce soit.
|
||||
|
||||
---
|
||||
|
||||
### Spot market
|
||||
|
||||
#### `NymeaEnergy.GetAvailableSpotMarketProviders`
|
||||
|
||||
@ -1,3 +1,22 @@
|
||||
powersync-energy-plugin-nymea (1.15.2+etm56) trixie; urgency=medium
|
||||
|
||||
* Divergence commande/mesure JOURNALISÉE à l'ouverture et à la fermeture
|
||||
(qCInfo). Elle n'était que publiée dans la télémétrie et tenue dans un QHash
|
||||
en mémoire : rien ne comptait les épisodes, le hash était purgé dès que
|
||||
l'écart cessait, et chaque redémarrage effaçait l'état. Le silence du journal
|
||||
était garanti par construction.
|
||||
* GetEnergyRatios / EnergyRatiosChanged publient trois champs de plus :
|
||||
periodStartedAt (epoch du minuit local), measuredSince (epoch de pose de la
|
||||
baseline) et timeZone (IANA). Toujours présents, même quand les deux taux
|
||||
sont omis.
|
||||
* periodStartedAt ≠ measuredSince : la baseline se repose à chaque redémarrage
|
||||
de nymead, donc en plein jour. Sans la paire, l'écran écrit « aujourd'hui »
|
||||
sur des chiffres qui ne parlent que de l'après-midi.
|
||||
* Le schéma des ratios est désormais déclaré UNE fois (energyRatiosSchema),
|
||||
servi au Get et au Changed — invariant 11.
|
||||
|
||||
-- ETM Schurig <dev@etm-schurig.eu> Sat, 05 Sep 2026 09:30:00 +0200
|
||||
|
||||
powersync-energy-plugin-nymea (1.15.2+etm55) trixie; urgency=medium
|
||||
|
||||
* §10 / LM-1013-b-v : sous le régime `unmeasurable`, la passe éco commande à
|
||||
|
||||
@ -1814,11 +1814,33 @@ void EnergyArbitrator::attachCommandDivergence(QVariantMap &entry, const QString
|
||||
// déphasage d'un cycle entre l'écriture et sa mesure. C'est pourtant TOUTE la différence
|
||||
// entre un fonctionnement normal et le cas relevé le 2026-08-31, où l'écart durait
|
||||
// indéfiniment parce que plus personne ne commandait.
|
||||
if (!divergent)
|
||||
// JOURNALISÉ AUX DEUX TRANSITIONS, et pas seulement publié.
|
||||
//
|
||||
// `divergent` et `since` ne vivaient que dans la charge utile de télémétrie et dans ce
|
||||
// QHash en mémoire — donc nulle part. Rien ne comptait les épisodes : un redémarrage de
|
||||
// nymead effaçait l'état, et le hash est PURGÉ dès que l'écart cesse, si bien qu'une
|
||||
// divergence qui s'ouvre et se referme entre deux lectures ne laissait aucune trace. Même
|
||||
// un observateur externe échantillonnant en continu aurait raté les épisodes courts, qui
|
||||
// sont probablement les plus nombreux.
|
||||
//
|
||||
// Sans ces deux lignes, « attendre que les traces parlent » attend indéfiniment : le
|
||||
// silence du journal était garanti par construction, pas par l'absence du phénomène.
|
||||
if (!divergent) {
|
||||
if (m_divergenceDepuis.contains(loadId)) {
|
||||
const qint64 dureeS = m_divergenceDepuis.value(loadId).secsTo(QDateTime::currentDateTime());
|
||||
qCInfo(dcNymeaEnergy()) << "[Divergence] FIN" << loadId
|
||||
<< "après" << dureeS << "s"
|
||||
<< "— attendu" << attenduW << "W, mesuré" << mesureW << "W";
|
||||
}
|
||||
m_divergenceDepuis.remove(loadId);
|
||||
else if (!m_divergenceDepuis.contains(loadId))
|
||||
m_divergenceDepuis.insert(loadId, m_lastTelemetryPublish.isValid()
|
||||
? m_lastTelemetryPublish : QDateTime::currentDateTime());
|
||||
} else if (!m_divergenceDepuis.contains(loadId)) {
|
||||
const QDateTime debut = m_lastTelemetryPublish.isValid()
|
||||
? m_lastTelemetryPublish : QDateTime::currentDateTime();
|
||||
m_divergenceDepuis.insert(loadId, debut);
|
||||
qCInfo(dcNymeaEnergy()) << "[Divergence] DÉBUT" << loadId
|
||||
<< "— attendu" << attenduW << "W, mesuré" << mesureW << "W,"
|
||||
<< "écart" << (mesureW - attenduW) << "W";
|
||||
}
|
||||
|
||||
QVariantMap commande;
|
||||
commande.insert(QStringLiteral("expectedW"), attenduW);
|
||||
|
||||
@ -24,6 +24,8 @@
|
||||
|
||||
#include "energyratioscalculator.h"
|
||||
|
||||
#include <QTimeZone>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
void EnergyRatiosCalculator::reset()
|
||||
@ -34,6 +36,9 @@ void EnergyRatiosCalculator::reset()
|
||||
m_baseConsumption = 0.0;
|
||||
m_baseAcquisition = 0.0;
|
||||
m_baseDay = 0;
|
||||
// Sans cette ligne, un `mesureDepuis` périmé survivrait au reset et daterait la mesure
|
||||
// d'avant l'installation qu'on vient de changer.
|
||||
m_baselinePosee = QDateTime();
|
||||
}
|
||||
|
||||
EnergyRatiosCalculator::Ratios EnergyRatiosCalculator::compute(double totalProduction,
|
||||
@ -59,6 +64,9 @@ EnergyRatiosCalculator::Ratios EnergyRatiosCalculator::compute(double totalProdu
|
||||
m_baseReturn = totalReturn;
|
||||
m_baseConsumption = totalConsumption;
|
||||
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;
|
||||
}
|
||||
|
||||
const double dProduction = totalProduction - m_baseProduction;
|
||||
@ -69,6 +77,11 @@ EnergyRatiosCalculator::Ratios EnergyRatiosCalculator::compute(double totalProdu
|
||||
Ratios result;
|
||||
result.autoconsommationValid = ratio(dProduction - dReturn, dProduction, &result.autoconsommation);
|
||||
result.autonomieValid = ratio(dConsumption - dAcquisition, dConsumption, &result.autonomie);
|
||||
// La frontière se construit dans le fuseau de `now` — le même que celui qui a servi à
|
||||
// décider du reseed. En prendre un autre ferait publier une frontière que le calcul
|
||||
// n'a pas utilisée.
|
||||
result.debutPeriode = QDateTime(now.date(), QTime(0, 0), now.timeZone());
|
||||
result.mesureDepuis = m_baselinePosee;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@ -57,6 +57,29 @@ public:
|
||||
double autoconsommation = 0.0; //!< Part de la production consommée sur place, % dans [0, 100].
|
||||
bool autonomieValid = false; //!< Faux = n/a (dénominateur nul) — ne pas afficher \c autonomie.
|
||||
double autonomie = 0.0; //!< Part de la consommation couverte sans soutirage, % dans [0, 100].
|
||||
|
||||
/*!
|
||||
* \brief Début de la PÉRIODE de reporting en cours — le minuit local qui l'ouvre.
|
||||
*
|
||||
* Publié plutôt que laissé à déduire : \c System.GetTime donne un nom IANA, et un
|
||||
* décalage ne s'en déduit pas sans base de fuseaux. Embarquer une base pour recalculer
|
||||
* une frontière que le moteur connaît déjà serait le mauvais geste — et le recalcul
|
||||
* resterait faux les jours de changement d'heure, où la journée fait 23 ou 25 heures.
|
||||
*/
|
||||
QDateTime debutPeriode;
|
||||
|
||||
/*!
|
||||
* \brief Instant où la BASELINE courante a été posée. Ce n'est PAS toujours
|
||||
* \c debutPeriode, et l'écart est la seule chose qui empêche les ratios de mentir.
|
||||
*
|
||||
* La baseline se repose à chaque redémarrage de nymead et à chaque recul de compteur.
|
||||
* Un jour où nymead a redémarré à 14 h, les ratios ne couvrent PAS la journée : ils
|
||||
* couvrent depuis 14 h. Publier la seule frontière de journée ferait afficher
|
||||
* « aujourd'hui » sur des chiffres qui ne parlent que de l'après-midi.
|
||||
*
|
||||
* Même paire, même raison que \c measuredSince / \c targetWh de l'avancement éco.
|
||||
*/
|
||||
QDateTime mesureDepuis;
|
||||
};
|
||||
|
||||
/*! \brief Construit sans baseline : le premier \c compute() la posera. */
|
||||
@ -91,6 +114,8 @@ private:
|
||||
static qint64 localDay(const QDateTime &now);
|
||||
|
||||
bool m_hasBaseline = false;
|
||||
//! Instant de pose de la baseline courante — publié en \c mesureDepuis.
|
||||
QDateTime m_baselinePosee;
|
||||
double m_baseProduction = 0.0;
|
||||
double m_baseReturn = 0.0;
|
||||
double m_baseConsumption = 0.0;
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
#include <energymanager.h>
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QTimeZone>
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||
|
||||
NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager, SmartChargingManager *smartChargingManager, LoadConfigStore *loadConfigStore, EnergyManager *energyManager, QObject *parent):
|
||||
@ -269,8 +270,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
||||
description = "Get the current energy ratios (self-consumption / autonomy) in percent, "
|
||||
"derived from the energy manager's cumulative counters over the current local "
|
||||
"day. A rate field is omitted (never null) when not available (denominator <= 0).";
|
||||
returns.insert("o:selfConsumptionRate", enumValueName(Double));
|
||||
returns.insert("o:autonomyRate", enumValueName(Double));
|
||||
returns = energyRatiosSchema();
|
||||
registerMethod("GetEnergyRatios", description, params, returns, Types::PermissionScopeControlThings);
|
||||
|
||||
// [ETM] ECS-410 — levée du verrou de défaut d'une charge. Délibérée et journalisée : le
|
||||
@ -434,8 +434,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
||||
params.clear();
|
||||
description = "Emitted whenever the energy ratios (self-consumption / autonomy) change. "
|
||||
"A rate field is omitted (never null) when not available.";
|
||||
params.insert("o:selfConsumptionRate", enumValueName(Double));
|
||||
params.insert("o:autonomyRate", enumValueName(Double));
|
||||
params = energyRatiosSchema();
|
||||
registerNotification("EnergyRatiosChanged", description, params);
|
||||
if (m_energyManager) {
|
||||
connect(m_energyManager, &EnergyManager::powerBalanceChanged,
|
||||
@ -754,6 +753,29 @@ JsonReply *NymeaEnergyJsonHandler::SetLoadConfig(const QVariantMap ¶ms)
|
||||
|
||||
// [Phase 2] Construit le map de ratios courant depuis les cumuls de l'energymanager.
|
||||
// Champ omis (jamais null) quand n/a. Met à jour l'état caché (m_lastRatios).
|
||||
QVariantMap NymeaEnergyJsonHandler::energyRatiosSchema()
|
||||
{
|
||||
QVariantMap champs;
|
||||
champs.insert("o:selfConsumptionRate", enumValueName(Double));
|
||||
champs.insert("o:autonomyRate", enumValueName(Double));
|
||||
// La FRONTIÈRE de la période, publiée plutôt que recalculée par chaque client. Trois
|
||||
// clients la recalculeraient de trois façons, et la seule qui compte est celle qui a servi
|
||||
// au calcul. `System.GetTime` donne un nom IANA : un décalage ne s'en déduit pas sans base
|
||||
// de fuseaux, et le recalcul resterait faux les jours de changement d'heure, où la journée
|
||||
// fait 23 ou 25 heures. Même règle que `counts`.
|
||||
champs.insert("periodStartedAt", enumValueName(Uint));
|
||||
// DEPUIS QUAND on mesure réellement — pas toujours la frontière. La baseline se repose à
|
||||
// chaque redémarrage de nymead et à chaque recul de compteur ; un jour où nymead a
|
||||
// redémarré à 14 h, ces ratios ne couvrent que l'après-midi. Sans ce champ, l'écran écrit
|
||||
// « aujourd'hui » sur des chiffres qui ne parlent pas de la journée. Même paire, même
|
||||
// raison que `measuredSince` / `targetWh` de l'avancement éco.
|
||||
champs.insert("measuredSince", enumValueName(Uint));
|
||||
// Le fuseau dans lequel la frontière a été construite. Il rend la période VÉRIFIABLE : sans
|
||||
// lui, un bilan journalier découpé au mauvais endroit se lit comme un bilan normal.
|
||||
champs.insert("timeZone", enumValueName(String));
|
||||
return champs;
|
||||
}
|
||||
|
||||
QVariantMap NymeaEnergyJsonHandler::currentEnergyRatios()
|
||||
{
|
||||
QVariantMap result;
|
||||
@ -774,6 +796,14 @@ QVariantMap NymeaEnergyJsonHandler::currentEnergyRatios()
|
||||
result.insert("selfConsumptionRate", r.autoconsommation);
|
||||
if (r.autonomieValid)
|
||||
result.insert("autonomyRate", r.autonomie);
|
||||
// Ces trois-là sont TOUJOURS présents, même quand les deux taux sont omis : « la période a
|
||||
// commencé à minuit et rien n'est encore mesurable » est une phrase vraie et utile, alors
|
||||
// qu'une réponse vide ne se distingue pas d'une panne.
|
||||
result.insert("periodStartedAt", static_cast<qint64>(r.debutPeriode.toSecsSinceEpoch()));
|
||||
result.insert("measuredSince",
|
||||
static_cast<qint64>((r.mesureDepuis.isValid() ? r.mesureDepuis : r.debutPeriode)
|
||||
.toSecsSinceEpoch()));
|
||||
result.insert("timeZone", QString::fromUtf8(QTimeZone::systemTimeZoneId()));
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -972,7 +1002,12 @@ void NymeaEnergyJsonHandler::onPowerBalanceChanged()
|
||||
|
||||
const bool moved = !hadPrev
|
||||
|| changed(prev.autoconsommationValid, prev.autoconsommation, now.autoconsommationValid, now.autoconsommation)
|
||||
|| changed(prev.autonomieValid, prev.autonomie, now.autonomieValid, now.autonomie);
|
||||
|| changed(prev.autonomieValid, prev.autonomie, now.autonomieValid, now.autonomie)
|
||||
// La bascule de période et la repose de baseline sont des changements de la charge
|
||||
// utile : ne pas les notifier laisserait un client connecté sur une frontière périmée
|
||||
// — le silence ambigu que l'invariant 11 interdit.
|
||||
|| prev.debutPeriode != now.debutPeriode
|
||||
|| prev.mesureDepuis != now.mesureDepuis;
|
||||
|
||||
if (moved)
|
||||
emit EnergyRatiosChanged(ratios);
|
||||
|
||||
@ -156,6 +156,11 @@ private:
|
||||
* celui qu'on ne teste pas tous les jours.
|
||||
*/
|
||||
static QVariantMap loadTelemetrySchema();
|
||||
|
||||
//! \brief Charge utile des ratios — DÉCLARÉE UNE FOIS, servie au \c Get et au \c Changed.
|
||||
//! Deux déclarations divergent, et c'est l'appel de reprise à froid qui casse (invariant 11).
|
||||
//! \return Le schéma des champs, prêt pour \c registerMethod et \c registerNotification.
|
||||
static QVariantMap energyRatiosSchema();
|
||||
//! \return Charge utile courante ; carte MINIMALE (sans \c timestamp) si aucun cycle
|
||||
//! d'arbitrage n'a encore eu lieu — cf. \c EnergyArbitrator::loadTelemetry().
|
||||
QVariantMap currentLoadTelemetry() const;
|
||||
|
||||
@ -650,6 +650,29 @@ Seul le second peut coûter de l'argent, et seul le second est **indevinable par
|
||||
fournisseur. Les fusionner en un unique « les fuseaux diffèrent » ferait disparaître celui des
|
||||
deux qui compte derrière celui qui se corrige tout seul.
|
||||
|
||||
**LM-1005-b-iv — la frontière d'une période de reporting se PUBLIE, elle ne se recalcule pas.**
|
||||
*(Demandé par l'agent app, posé le 2026-09-05.)*
|
||||
|
||||
`GetEnergyRatios` publie `periodStartedAt` (epoch), `measuredSince` (epoch) et `timeZone` (IANA).
|
||||
|
||||
**Pourquoi publier plutôt que laisser déduire.** `System.GetTime` donne un NOM de fuseau ; un
|
||||
décalage ne s'en déduit pas sans base de fuseaux. Embarquer une base pour recalculer une frontière
|
||||
que le moteur connaît déjà serait le mauvais geste — et le recalcul resterait **faux les jours de
|
||||
changement d'heure**, où la journée fait 23 ou 25 heures. Trois clients la recalculeraient de
|
||||
trois façons, et la seule qui compte est celle qui a servi au calcul. **Même règle que `counts`.**
|
||||
|
||||
**Et c'est la PARADE à la troisième nature.** La période de reporting suit l'installation, ce qui
|
||||
est correct — mais repose sur la prémisse que le fuseau de la box soit celui de l'installation, et
|
||||
c'était le seul des trois cas où **personne ne vérifiait**. Publier la frontière et son fuseau est
|
||||
exactement ce qui rend la vérification possible.
|
||||
|
||||
**LM-1005-b-v — `periodStartedAt` et `measuredSince` sont DEUX champs, et les confondre fait
|
||||
mentir les ratios.** La baseline se repose à chaque redémarrage de nymead et à chaque recul de
|
||||
compteur — donc en plein jour. Un jour où nymead a redémarré à 14 h, les ratios ne couvrent pas la
|
||||
journée : ils couvrent depuis 14 h. Avec la seule frontière, l'écran écrit « aujourd'hui » sur des
|
||||
chiffres qui ne parlent que de l'après-midi. Même paire, même raison que
|
||||
`progress.measuredSince` / `targetWh`.
|
||||
|
||||
**Conséquence de conception** : la passe 1 n'est **pas** bornée par le surplus. Elle est bornée
|
||||
par une **autorisation de soutirage** — un objet distinct, que le rule-based rendra binaire et
|
||||
que le MILP rendra continu. La passe 2, elle, reste strictement bornée par le reliquat de
|
||||
|
||||
@ -977,6 +977,41 @@ void Simulation::testEnergyRatiosAlignment()
|
||||
// F — même jour (30) : auto=(600-200)/600≈66.667%, autonomie=(1000-500)/1000=50%.
|
||||
EnergyRatiosCalculator::Ratios f = calc.compute(2600, 700, 7000, 4500, d30_10);
|
||||
eqAuto(f, 66.6667); eqAuton(f, 50.0);
|
||||
|
||||
// ── LA FRONTIÈRE DE PÉRIODE ET L'INSTANT DE MESURE, qui ne sont PAS le même instant ──
|
||||
//
|
||||
// Publier la seule frontière ferait écrire « aujourd'hui » sur des chiffres qui ne parlent
|
||||
// pas de la journée : la baseline se repose à chaque redémarrage de nymead et à chaque
|
||||
// recul de compteur, donc en plein jour. C'est la paire qui dit la vérité, pas le champ.
|
||||
const QDateTime minuit29(QDate(2026, 6, 29), QTime(0, 0));
|
||||
const QDateTime minuit30(QDate(2026, 6, 30), QTime(0, 0));
|
||||
|
||||
// Cas NORMAL — pose à minuit : les deux coïncident, et c'est ce qui rend l'écart lisible
|
||||
// quand il apparaît.
|
||||
QCOMPARE(a.debutPeriode, minuit29);
|
||||
QCOMPARE(a.mesureDepuis, d29_00);
|
||||
|
||||
// Sans repose, l'instant de mesure NE BOUGE PAS : il date la baseline, pas le calcul.
|
||||
QCOMPARE(b.debutPeriode, minuit29);
|
||||
QCOMPARE(b.mesureDepuis, d29_00);
|
||||
|
||||
// REPOSE EN PLEIN JOUR (compteur non monotone à 10:00) — LES DEUX DIVERGENT. C'est le cas
|
||||
// qui justifie la paire : la frontière dit toujours minuit, la mesure ne couvre que depuis
|
||||
// 10 h, et un écran qui n'aurait que la frontière l'affirmerait à tort sur la journée.
|
||||
QCOMPARE(d.debutPeriode, minuit29);
|
||||
QCOMPARE(d.mesureDepuis, d29_10);
|
||||
QVERIFY2(d.mesureDepuis > d.debutPeriode,
|
||||
"une repose en plein jour doit se VOIR : sinon les ratios mentent sur leur portée");
|
||||
|
||||
// BASCULE DE JOUR — la frontière suit le nouveau jour, et la mesure date du premier calcul
|
||||
// APRÈS minuit, jamais de minuit lui-même. Sur une box qui calcule en continu l'écart est
|
||||
// de quelques secondes ; l'affirmer nul serait une approximation que rien n'oblige à faire.
|
||||
QCOMPARE(e.debutPeriode, minuit30);
|
||||
QCOMPARE(e.mesureDepuis, d30_02);
|
||||
|
||||
// Et la frontière ne rebouge pas dans la journée.
|
||||
QCOMPARE(f.debutPeriode, minuit30);
|
||||
QCOMPARE(f.mesureDepuis, d30_02);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user