feat(R2): le financement descend au niveau — funding vit dans levels[]
funding é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. 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 : seule des huit exigences de la maquette à être un changement de FORME d'un champ existant, donc seule à se renchérir à chaque écran qui s'y appuie. UN SEUL NIVEAU aujourd'hui, 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. C'est ce qui rend le déplacement faisable avant le lot : la forme est juste dès maintenant. funding N'EST PAS conservé en résumé sur la charge, et c'est la première des deux questions posées. Le cas mixte est justement celui qui le rendrait indécidable : « mixte » serait un troisième code que personne n'a demandé, « grid si l'un l'est » une convention que chaque client redériverait autrement. Deux endroits pour un fait divergent. Le motif `decision` reste, lui, au niveau de la charge — parce qu'on saura dire de quel niveau il parle (R7). L'asymétrie est la règle : on conserve un champ hérité QUAND ON PEUT EN DIRE QUELQUE CHOSE DE VRAI, jamais par symétrie. L'IDENTITÉ DE RÉCONCILIATION est écrite dans INTERFACE.md, seconde question posée. Somme sur les charges, somme sur les niveaux dont funding == "surplus", de niveau.targetW == budget.allocatedW. Un client qui la reconstruirait depuis l'ancienne forme divergerait au premier cas mixte. Et son unique exception est publiée plutôt que sue : EV_GRID_START partage une allocation entre deux compteurs, sa contribution au surplus est decision.params.budgetW et non targetW. C'est ce que R3 généralisera ; d'ici là l'exception vit dans le contrat, pas dans la tête du lecteur. levels[] absent = box antérieure OU mode dégradé — le repli de l'app couvre les deux. Rupture assumée pour un client lisant entry.funding : même arbitrage que measuredW → measurement. simulation 33/33, charging 16/16, loadmodel 20/20, amd64 0/0, doxygen 0. 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
459ba9b009
commit
260534d648
@ -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());
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user