diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp index 0f8e16a..11c0ae1 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -1048,7 +1048,12 @@ void EnergyArbitrator::buildTelemetry(const Plan &plan, const Slot &slot, const QVariantMap orphan; orphan.insert(QStringLiteral("loadId"), action.loadId); orphan.insert(QStringLiteral("allocatedW"), 0); - orphan.insert(QStringLiteral("funding"), QStringLiteral("surplus")); + // R2 — le financement vit dans levels[], plus sur la charge. Une charge dont + // l'appareil manque n'a rien décidé : un seul niveau, à 0 W. + orphan.insert(QStringLiteral("levels"), QVariantList() + << QVariantMap{{QStringLiteral("level"), QStringLiteral("comfort")}, + {QStringLiteral("targetW"), 0.0}, + {QStringLiteral("funding"), QStringLiteral("surplus")}}); orphan.insert(QStringLiteral("available"), false); orphan.insert(QStringLiteral("decision"), action.reason.toMap()); orphan.insert(QStringLiteral("faultCode"), QStringLiteral("THING_MISSING")); @@ -1067,15 +1072,36 @@ void EnergyArbitrator::buildTelemetry(const Plan &plan, const Slot &slot, const // vit dans la charge utile de mécanisme ; les confondre rendrait la somme des // allocations irréconciliable avec budget.allocatedW. entry.insert(QStringLiteral("allocatedW"), action.estimatedPowerW); - // Le FINANCEMENT, sans quoi la charge utile n'est pas réconciliable. Seules les actions - // financées au SURPLUS ont décrémenté la cascade et sont comptées dans - // budget.allocatedW ; celles financées au RÉSEAU — échéance et spot market, restées au - // proxy — portent une puissance qui vit dans budget.evReservedW. Sans ce champ, un - // client qui somme loads[].allocatedW trouve un écart et n'a aucun moyen de savoir - // s'il regarde un bug ou une borne servie au réseau. - entry.insert(QStringLiteral("funding"), - action.funding == LoadAction::Grid ? QStringLiteral("grid") - : QStringLiteral("surplus")); + // --- R2 : LE FINANCEMENT DESCEND AU NIVEAU ------------------------------------- + // + // Il était par CHARGE. Au §10 la même charge sera financée au réseau pour son plancher + // éco et au surplus pour son confort DANS LE MÊME CYCLE : un financement par charge y + // devient indécidable, et c'est lui qui porte l'identité de réconciliation de l'app + // (« somme des parts financées au surplus == budget.allocatedW »). Laissé à la charge, + // cette identité cesserait d'être vérifiable sans que rien ne le signale. + // + // Déplacé MAINTENANT bien que le §10 ne soit pas écrit : c'est la seule des huit + // exigences de la maquette qui soit un changement de FORME d'un champ existant, donc la + // seule qui se renchérisse à chaque écran qui s'y appuie. + // + // Aujourd'hui UN SEUL niveau, et c'est « comfort » : la passe unique d'aujourd'hui est + // celle qui, au §10, servira le confort depuis le surplus. La passe éco est la + // nouveauté du lot, pas la passe existante rebaptisée. Une charge sans obligation éco + // n'aura jamais d'entrée « eco » — l'absence est l'information (R1). + // + // « funding » n'est PAS conservé en résumé sur la charge : voir INTERFACE.md. Le cas + // mixte est justement celui qui le rend indécidable, et deux endroits pour un fait + // divergent. Le motif « decision », lui, RESTE — parce qu'on saura dire de quel niveau + // il parle (R7), là où un financement de charge ne pourrait rien dire de vrai. + QVariantMap niveau; + niveau.insert(QStringLiteral("level"), QStringLiteral("comfort")); + // R4 — le niveau publie ce qu'il a DÉCIDÉ. C'est la même valeur qu'allocatedW tant + // qu'il n'y a qu'un niveau ; elle s'en séparera quand il y en aura deux. + niveau.insert(QStringLiteral("targetW"), action.estimatedPowerW); + niveau.insert(QStringLiteral("funding"), + action.funding == LoadAction::Grid ? QStringLiteral("grid") + : QStringLiteral("surplus")); + entry.insert(QStringLiteral("levels"), QVariantList() << niveau); entry.insert(QStringLiteral("available"), adapter->telemetry().available); entry.insert(QStringLiteral("decision"), action.reason.toMap()); diff --git a/energyplugin/nymeaenergyjsonhandler.cpp b/energyplugin/nymeaenergyjsonhandler.cpp index 4d31057..1396594 100644 --- a/energyplugin/nymeaenergyjsonhandler.cpp +++ b/energyplugin/nymeaenergyjsonhandler.cpp @@ -757,6 +757,16 @@ QVariantMap NymeaEnergyJsonHandler::loadTelemetrySchema() measurementItem.insert("source", enumValueName(String)); // meter | device | none measurementItem.insert("o:powerW", enumValueName(Double)); + // §10 / R1-R2-R4 — un élément par passe RÉELLEMENT parcourue. Aujourd'hui il y en a un + // seul, « comfort » : la passe unique d'aujourd'hui est celle qui servira le confort depuis + // le surplus. Une charge sans obligation éco n'aura jamais d'entrée « eco » à zéro — + // l'absence est l'information. « targetW » est ce que le niveau a DÉCIDÉ (R4), à ne pas + // confondre avec la charge utile de mécanisme, qui dit ce qui a été APPLIQUÉ. + QVariantMap levelItem; + levelItem.insert("level", enumValueName(String)); // eco | comfort + levelItem.insert("targetW", enumValueName(Double)); + levelItem.insert("funding", enumValueName(String)); // surplus | grid + QVariantMap loadItem; loadItem.insert("loadId", enumValueName(String)); // ALLOCATION du waterfall (ce qui a décrémenté le budget), pas la mesure : la mesure vit @@ -769,7 +779,11 @@ QVariantMap NymeaEnergyJsonHandler::loadTelemetrySchema() // au réseau et dont la puissance vit dans budget.evReservedW. Sans ce champ, un client qui // somme allocatedW trouve un écart sans pouvoir dire s'il regarde un défaut ou une borne // servie au réseau. Omis en mode dégradé : sans plan, il n'y a pas de financement. - loadItem.insert("o:funding", enumValueName(String)); + // R2 — le financement descend au NIVEAU (levels[]). Il n'est PAS conservé en résumé sur la + // charge : au §10 la même charge sera financée au réseau pour son éco et au surplus pour + // son confort dans le même cycle, et un résumé n'aurait alors aucune valeur vraie à porter. + // Deux endroits pour un fait divergent. + loadItem.insert("levels", QVariantList() << levelItem); loadItem.insert("available", enumValueName(Bool)); loadItem.insert("o:faultCode", enumValueName(String)); // absent = aucun défaut // §11 (LM-1101/1103) — mesures de la charge, publiées à côté de l'allocation et JAMAIS diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index 57fd552..5f15928 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -5323,7 +5323,16 @@ void Simulation::testEvGridStartIsAnnouncedAndBounded() QCOMPARE(qRound(pA.value("budgetW").toDouble() + pA.value("gridW").toDouble()), 1380); // Le financement DIT la vérité sur l'origine : une part vient du réseau. - QCOMPARE(tele.value(borneAId.toString()).value("funding").toString(), QString("grid")); + // R2 — le financement vit dans levels[], plus sur la charge. Un seul niveau aujourd'hui. + const QVariantList nivA = tele.value(borneAId.toString()).value("levels").toList(); + QCOMPARE(nivA.count(), 1); + QCOMPARE(nivA.first().toMap().value("level").toString(), QString("comfort")); + QCOMPARE(nivA.first().toMap().value("funding").toString(), QString("grid")); + // Et le niveau publie ce qu'il a DÉCIDÉ : tant qu'il n'y en a qu'un, c'est allocatedW. + QCOMPARE(qRound(nivA.first().toMap().value("targetW").toDouble()), + qRound(tele.value(borneAId.toString()).value("allocatedW").toDouble())); + QVERIFY2(!tele.value(borneAId.toString()).contains("funding"), + "funding ne doit plus être publié au niveau de la CHARGE"); // ---- Borné par construction : UNE borne par cycle -------------------------------------- // Le démarrage consomme TOUT le budget restant ; la suivante trouve zéro et repart sur son