test(mock): le compteur intègre enfin, et purchasedWh gagne un second usage
totalEnergyConsumed n'était écrit par personne : il valait sa valeur par défaut, indéfiniment. Un compteur figé est un compteur qui n'existe pas pour tout ce qui l'intègre — deliveredWh restait à 0, remainingWh à 4 000, et l'étalement adaptatif n'était jamais exercé. Le comportement se confondait avec unmeasurable pour une raison entièrement différente et bénigne. Quatrième occurrence d'un mock plus homogène que la réalité, après les six états cohérents du rootmeter, les classes portant toutes une puissance, et la triphasée équilibrée. En export il ne recule pas — il ne bouge pas : un compteur de consommation est monotone, et le faire reculer produirait un cas que le terrain ne produit pas, celui-là même que la garde de sémantique traite comme une anomalie. Le mock fabriquerait le défaut que le moteur est censé détecter. LM-1014-b appliquée au mock, question posée et répondue : le cumul est déclaré `cached`, donc il continue au redémarrage. Repartir de zéro ferait reculer un cumul monotone. Et purchasedWh gagne un second usage, sous `measured` cette fois. Mesuré sur le banc : il accumule pendant que deliveredWh peut rester plat, et cette divergence est un contrôle que l'écran n'aurait pas su fabriquer — de l'acheté qui monte avec du livré plat signifie soit que la charge ne consomme pas ce qu'on paie, soit que le compteur ne compte pas. C'est écrit au contrat parce que le jour où les deux coïncideront, quelqu'un jugera le champ redondant : c'est précisément quand ils divergent que la paire devient informative, et un champ qui ne sert que dans l'anomalie n'a pas l'air utile le reste du temps. La borne basse du test de cumul est large, et délibérément : il ne contrôle ni la cadence du minuteur ni l'alignement du premier tic, qui n'intègre rien. Une borne serrée en ferait un test instable — qu'on relancerait jusqu'à ce qu'il passe. Suite : simulation 66/66, 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
c88ba6ad45
commit
04852c518a
15
INTERFACE.md
15
INTERFACE.md
@ -703,6 +703,21 @@ Sans lui, « ce que ça coûte par jour » redeviendrait une estimation fabriqu
|
||||
C'est un **cumul sur la fenêtre d'obligation** : remis à zéro à la bascule de période
|
||||
(`LM-1014-b`).
|
||||
|
||||
> ### Un SECOND usage, sous `measured` cette fois — et il n'est pas redondant
|
||||
>
|
||||
> Le champ a été demandé pour `unmeasurable`. Mesuré sur le banc, il gagne un usage que personne
|
||||
> n'avait prévu : sous `measured`, `purchasedWh` **accumule** — 0 → 9,15 → 18,33 → 27,52 Wh —
|
||||
> pendant que `deliveredWh` peut rester **plat**.
|
||||
>
|
||||
> **Cette divergence est un contrôle que l'écran n'aurait pas su fabriquer.** De l'acheté qui
|
||||
> monte avec du livré plat signifie l'une de deux choses, et les deux méritent d'être vues :
|
||||
> soit **la charge ne consomme pas ce qu'on paie**, soit **le compteur ne compte pas**.
|
||||
>
|
||||
> C'est écrit ici parce que le jour où les deux valeurs coïncideront, quelqu'un jugera le champ
|
||||
> redondant et proposera de le retirer. Il ne l'est pas : **c'est précisément quand ils
|
||||
> divergent que la paire devient informative**, et un champ qui ne sert que dans l'anomalie n'a
|
||||
> pas l'air utile le reste du temps.
|
||||
|
||||
> ### `measuredSince` ferme un piège d'affichage, et il faut savoir lequel
|
||||
>
|
||||
> Quand une obligation bascule de `unmeasurable` à `measured` **en cours de période**, la base
|
||||
|
||||
@ -1,3 +1,34 @@
|
||||
powersync-energy-plugin-nymea (1.15.2+etm54) trixie; urgency=medium
|
||||
|
||||
* +etm53 DÉPLOYÉ sur .75 le 2026-09-04, version relue et `nymead` actif : `bindingPhase` et le
|
||||
correctif amont de la tension sont sur la box.
|
||||
* MOCK — LE COMPTEUR INTÈGRE ENFIN. `totalEnergyConsumed` n'était écrit par personne : il
|
||||
valait sa valeur par défaut indéfiniment. Un compteur figé est un compteur qui N'EXISTE PAS
|
||||
pour tout ce qui l'intègre — `deliveredWh` restait à 0, `remainingWh` à 4 000, et l'étalement
|
||||
adaptatif n'était JAMAIS exercé. Le comportement se confondait avec `unmeasurable` pour une
|
||||
raison entièrement différente et bénigne. QUATRIÈME occurrence d'un mock plus homogène que la
|
||||
réalité.
|
||||
* EN EXPORT IL NE RECULE PAS, il ne bouge pas : un compteur de consommation est monotone, et
|
||||
le faire reculer produirait un cas que le terrain ne produit pas — celui que la garde de
|
||||
sémantique traite comme une anomalie. Le mock fabriquerait le défaut que le moteur détecte.
|
||||
* LM-1014-b APPLIQUÉE AU MOCK, question posée et répondue : `totalEnergyConsumed` est déclaré
|
||||
`cached`, donc il CONTINUE au redémarrage. Repartir de zéro ferait reculer un cumul monotone.
|
||||
* CONTRAT — `purchasedWh` gagne un SECOND USAGE, sous `measured` cette fois, et il n'est pas
|
||||
redondant. Mesuré : il accumule (0 → 9,15 → 18,33 → 27,52 Wh) pendant que `deliveredWh` peut
|
||||
rester PLAT. Cette divergence est un contrôle que l'écran n'aurait pas su fabriquer : de
|
||||
l'acheté qui monte avec du livré plat signifie soit que la charge ne consomme pas ce qu'on
|
||||
paie, soit que le compteur ne compte pas. Écrit parce que le jour où les deux coïncideront,
|
||||
quelqu'un jugera le champ redondant — or c'est précisément quand ils DIVERGENT que la paire
|
||||
devient informative, et un champ qui ne sert que dans l'anomalie n'a pas l'air utile le reste
|
||||
du temps.
|
||||
* Le test du cumul a une borne basse LARGE, et c'est délibéré : il ne contrôle ni la cadence du
|
||||
minuteur ni l'alignement du premier tic, qui n'intègre rien. Une borne serrée en ferait un
|
||||
test instable — c'est-à-dire un test qu'on relancerait jusqu'à ce qu'il passe.
|
||||
* Suite : simulation 66/66, loadmodel 22/22, charging 48/48 identique à la référence,
|
||||
spotmarket 32/32, doxygen 0 avertissement.
|
||||
|
||||
-- Patrick Schurig <etm.schurig@gmail.com> Fri, 04 Sep 2026 14:00:00 +0200
|
||||
|
||||
powersync-energy-plugin-nymea (1.15.2+etm53) trixie; urgency=medium
|
||||
|
||||
* [upstream-fix] rootmeter — LA TENSION EST DÉSORMAIS LOCALE À CHAQUE PHASE. `voltage` était
|
||||
|
||||
@ -87,6 +87,24 @@ des contrôles — avant d'être comblé. C'est ce qui lui a donné sa priorité
|
||||
combien on en a examinés**, et échouer si le compte est nul. C'est ce qui a rattrapé le test de
|
||||
`command`, et ce qui garde celui de la mesurabilité.
|
||||
|
||||
**5. ~~Un compteur d'énergie FIGÉ.~~ — OUVERT le 2026-09-04.** `totalEnergyConsumed` n'était
|
||||
écrit par personne : il valait sa valeur par défaut, indéfiniment. **Un compteur figé est un
|
||||
compteur qui n'existe pas pour tout ce qui l'intègre** — `deliveredWh` restait à 0, `remainingWh`
|
||||
à 4 000, et l'étalement adaptatif n'était **jamais exercé**. Le comportement se confondait avec
|
||||
`unmeasurable` pour une raison entièrement différente et bénigne.
|
||||
|
||||
Le mock intègre désormais `currentPower` dans le cumul, une fois par seconde. **En export il ne
|
||||
recule pas — il ne bouge pas** : un compteur de consommation est monotone, et le faire reculer
|
||||
produirait un cas que le terrain ne produit pas, que la garde de sémantique traiterait comme une
|
||||
anomalie.
|
||||
|
||||
**Et `LM-1014-b` s'y applique, question posée et répondue** : `totalEnergyConsumed` est déclaré
|
||||
`cached`, donc il **continue** au redémarrage du mock. Repartir de zéro ferait reculer un cumul
|
||||
monotone — le mock fabriquerait l'anomalie que le moteur est censé détecter.
|
||||
|
||||
**Quatrième occurrence.** Après les six états cohérents du `rootmeter`, les classes portant toutes
|
||||
une puissance, et la triphasée équilibrée.
|
||||
|
||||
## Ce qu'il faudrait ajouter, par ordre d'utilité
|
||||
|
||||
1. **Une classe pilotable SANS aucune puissance** — un relais sec. Débloque `source: none` sur
|
||||
|
||||
@ -7791,6 +7791,68 @@ void Simulation::testPhaseVoltageDoesNotLeakBetweenPhases()
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Le compteur du mock INTÈGRE réellement — un cumul figé n'existe pas.
|
||||
*
|
||||
* \par Le blocage qu'il lève
|
||||
* `totalEnergyConsumed` valait **10 kWh depuis le 1er septembre** : une constante. `deliveredWh`
|
||||
* restait donc à 0, `remainingWh` à 4 000, et **l'étalement adaptatif n'était jamais exercé** —
|
||||
* le comportement se confondait avec `unmeasurable` pour une raison entièrement différente et
|
||||
* bénigne. Un compteur figé est un compteur qui n'existe pas pour tout ce qui l'intègre.
|
||||
*
|
||||
* \par \rule{LM-1014-b} appliquée au mock lui-même
|
||||
* `totalEnergyConsumed` est déclaré **`cached`** : c'est un compteur de VIE, il **continue** au
|
||||
* redémarrage et ne repart pas de zéro. Repartir de zéro ferait **reculer un cumul monotone**,
|
||||
* ce que la garde de sémantique traite comme une anomalie — le mock produirait alors un cas que
|
||||
* le terrain ne produit pas.
|
||||
*/
|
||||
void Simulation::testMeterEnergyActuallyAccumulates()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
|
||||
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||||
QUuid meterId = addMeter(26693);
|
||||
Thing *meter = tm->findConfiguredThing(meterId);
|
||||
QVERIFY(meter);
|
||||
meter->setStateValue("connected", true);
|
||||
|
||||
// Le compteur CONSOMME 3 600 W : un watt-heure par seconde, de quoi voir bouger le cumul.
|
||||
meter->setStateValue("currentPower", 3600);
|
||||
const double depart = meter->stateValue("totalEnergyConsumed").toDouble();
|
||||
|
||||
QTest::qWait(2500);
|
||||
const double apres = meter->stateValue("totalEnergyConsumed").toDouble();
|
||||
QVERIFY2(apres > depart,
|
||||
qUtf8Printable(QStringLiteral("le cumul doit AVANCER : %1 → %2 kWh")
|
||||
.arg(depart).arg(apres)));
|
||||
|
||||
// …et il avance dans un ORDRE DE GRANDEUR plausible : 3 600 W pendant ~2,5 s, soit ~2,5 Wh
|
||||
// si toutes les secondes intègrent. La borne basse est LARGE et volontairement lâche : le
|
||||
// test ne contrôle ni la cadence exacte du minuteur ni l'alignement du premier tic — qui
|
||||
// n'intègre rien, faute de durée derrière lui. Une borne serrée en ferait un test instable,
|
||||
// c'est-à-dire un test qu'on relancerait jusqu'à ce qu'il passe.
|
||||
//
|
||||
// Ce qui est réellement épinglé : il intègre, et il n'emballe pas. Le premier point est ce
|
||||
// qui manquait ; le second est ce qui rendrait le mock inutilisable dans l'autre sens.
|
||||
const double gagneWh = (apres - depart) * 1000.0;
|
||||
QVERIFY2(gagneWh >= 0.5 && gagneWh < 10.0,
|
||||
qUtf8Printable(QStringLiteral("ordre de grandeur plausible attendu (~2,5 Wh), "
|
||||
"obtenu %1").arg(gagneWh)));
|
||||
|
||||
// EN EXPORT, il ne recule pas — il ne bouge pas. Un compteur de CONSOMMATION est monotone,
|
||||
// et le faire reculer produirait un cas que le terrain ne produit pas.
|
||||
meter->setStateValue("currentPower", -3000);
|
||||
const double avantExport = meter->stateValue("totalEnergyConsumed").toDouble();
|
||||
QTest::qWait(1500);
|
||||
QCOMPARE(meter->stateValue("totalEnergyConsumed").toDouble(), avantExport);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testCountsSumToTarget()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
|
||||
@ -203,6 +203,7 @@ private slots:
|
||||
void testLifetimeCounterDropNeverRebasesTheProgress();
|
||||
void testWorstPhaseBindsWhenPhasesAreUnbalanced();
|
||||
void testPhaseVoltageDoesNotLeakBetweenPhases();
|
||||
void testMeterEnergyActuallyAccumulates();
|
||||
void testRecreditIsALoanAgainstObedience();
|
||||
void testARefusedStateIsDistinguishableFromAnApplied();
|
||||
void testL4ShedsAPacAndSaysItForcedTheLock();
|
||||
|
||||
@ -36,8 +36,44 @@ IntegrationPluginEnergyMocks::IntegrationPluginEnergyMocks(QObject *parent) : In
|
||||
|
||||
}
|
||||
|
||||
void IntegrationPluginEnergyMocks::integrerEnergie()
|
||||
{
|
||||
const QDateTime maintenant = QDateTime::currentDateTime();
|
||||
for (Thing *thing : myThings()) {
|
||||
if (!thing->thingClass().hasStateType("totalEnergyConsumed")
|
||||
|| !thing->thingClass().hasStateType("currentPower"))
|
||||
continue;
|
||||
|
||||
const QDateTime avant = m_derniereIntegration.value(thing);
|
||||
m_derniereIntegration.insert(thing, maintenant);
|
||||
// Première passe : aucune durée derrière elle. Intégrer y fabriquerait de l'énergie.
|
||||
if (!avant.isValid())
|
||||
continue;
|
||||
|
||||
const double heures = avant.msecsTo(maintenant) / 3600000.0;
|
||||
const double puissanceW = thing->stateValue("currentPower").toDouble();
|
||||
// Un compteur de CONSOMMATION n'intègre que ce qui est consommé : en export, il ne
|
||||
// recule pas — il ne bouge pas. Le faire reculer produirait un cas que le terrain ne
|
||||
// produit pas, et que la garde de sémantique traiterait comme une anomalie.
|
||||
if (puissanceW <= 0 || heures <= 0)
|
||||
continue;
|
||||
|
||||
const double avantKWh = thing->stateValue("totalEnergyConsumed").toDouble();
|
||||
thing->setStateValue("totalEnergyConsumed", avantKWh + puissanceW * heures / 1000.0);
|
||||
}
|
||||
}
|
||||
|
||||
void IntegrationPluginEnergyMocks::setupThing(ThingSetupInfo *info)
|
||||
{
|
||||
// Intégrateur d'énergie, armé une fois. Une seconde : assez fin pour qu'un cycle
|
||||
// d'arbitrage (à la minute) voie le compteur BOUGER, ce qui est tout l'objet.
|
||||
if (!m_integrateur) {
|
||||
m_integrateur = new QTimer(this);
|
||||
m_integrateur->setInterval(1000);
|
||||
connect(m_integrateur, &QTimer::timeout, this, &IntegrationPluginEnergyMocks::integrerEnergie);
|
||||
m_integrateur->start();
|
||||
}
|
||||
|
||||
Thing *thing = info->thing();
|
||||
qCDebug(dcEnergyMocks()) << "Setting up" << thing << thing->params();
|
||||
|
||||
|
||||
@ -26,6 +26,9 @@
|
||||
#define INTEGRATIONPLUGINENERGYMOCKS_H
|
||||
|
||||
#include "integrations/integrationplugin.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
|
||||
#include "extern-plugininfo.h"
|
||||
@ -48,6 +51,31 @@ public:
|
||||
private:
|
||||
QHash<Thing*, EnergyMockController *> m_controllers;
|
||||
|
||||
/*!
|
||||
* \brief Intègre `currentPower` dans `totalEnergyConsumed`, pour TOUS les compteurs.
|
||||
*
|
||||
* \par Pourquoi le mock doit le faire
|
||||
* Le compteur du banc valait **10 kWh depuis le 1er septembre** : une constante, pas un
|
||||
* cumul. `deliveredWh` restait donc à 0 et `remainingWh` à 4 000 — l'étalement adaptatif
|
||||
* n'était **jamais exercé**, et le comportement se confondait avec `unmeasurable` pour une
|
||||
* raison entièrement différente et bénigne.
|
||||
*
|
||||
* **Un compteur figé est un compteur qui n'existe pas pour tout ce qui l'intègre.** Quatrième
|
||||
* occurrence d'un mock plus homogène que la réalité (`docs/INVENTAIRE_MOCK.md`).
|
||||
*
|
||||
* \note `totalEnergyConsumed` est déclaré **`cached`** : c'est un compteur de VIE, il
|
||||
* **continue** au redémarrage du mock et ne repart pas de zéro (\rule{LM-1014-b} : la
|
||||
* question posée, et la réponse choisie explicitement). Repartir de zéro ferait reculer un
|
||||
* cumul monotone, ce que la garde de sémantique traite justement comme une anomalie —
|
||||
* le mock produirait alors un cas que le terrain ne produit pas.
|
||||
*/
|
||||
void integrerEnergie();
|
||||
|
||||
//! Instant de la dernière intégration, par Thing. Absent = première passe, qui n'intègre
|
||||
//! rien : sans durée derrière elle, elle fabriquerait de l'énergie.
|
||||
QHash<Thing*, QDateTime> m_derniereIntegration;
|
||||
QTimer *m_integrateur = nullptr;
|
||||
|
||||
// Update the meter data of the charger based on the current max charging current, connected, power and plugged in state
|
||||
void updateChargerMeter(Thing *thing);
|
||||
|
||||
|
||||
@ -133,7 +133,8 @@
|
||||
"displayName": "Total energy consumed",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0.0
|
||||
"defaultValue": 0.0,
|
||||
"cached": true
|
||||
},
|
||||
{
|
||||
"id": "0420b758-e77f-4cf5-a30b-a6e1235b1efd",
|
||||
@ -141,7 +142,8 @@
|
||||
"displayName": "Total energy produced",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0.0
|
||||
"defaultValue": 0.0,
|
||||
"cached": true
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -238,7 +240,8 @@
|
||||
"displayName": "Total energy consumed",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0.0
|
||||
"defaultValue": 0.0,
|
||||
"cached": true
|
||||
},
|
||||
{
|
||||
"id": "5a8f841d-60d2-443f-81bf-61b678268307",
|
||||
@ -246,7 +249,8 @@
|
||||
"displayName": "Total energy produced",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0.0
|
||||
"defaultValue": 0.0,
|
||||
"cached": true
|
||||
},
|
||||
{
|
||||
"id": "c5b2b28b-11b4-4274-84ea-17786281201a",
|
||||
@ -339,7 +343,8 @@
|
||||
"displayName": "Total consumed energy",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0
|
||||
"defaultValue": 0,
|
||||
"cached": true
|
||||
},
|
||||
{
|
||||
"id": "13f8f008-aa70-4772-9fb8-81a9674dd6ad",
|
||||
@ -554,7 +559,8 @@
|
||||
"displayName": "Total consumed energy",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0
|
||||
"defaultValue": 0,
|
||||
"cached": true
|
||||
},
|
||||
{
|
||||
"id": "7c1e4a92-3f6b-4d18-9a54-2b8e0d5f1c73",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user