From 0638008a9f2db818a044e01ead970cb323c04485 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sun, 30 Aug 2026 08:58:11 +0200 Subject: [PATCH] =?UTF-8?q?feat(R3):=20draw.committedW=20=E2=80=94=20le=20?= =?UTF-8?q?registre=20des=20watts=20achet=C3=A9s,=20pr=C3=A9alable=20?= =?UTF-8?q?=C3=A0=20counts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'app a validé le préalable sans réserve, et sa formulation de pourquoi counts n'a pas été livré mérite d'être gardée : une identité dépendante du temps est pire que pas d'identité. Vraie au premier cycle après un changement, fausse après convergence, elle passe tous les tests — qui portent sur des cycles isolés — et certifie un total faux chez les clients, avec le sceau d'une identité publiée. draw.committedW est publié dans la MÊME TRAME que budget : les deux décrivent le même cycle, et les lire à deux instants ferait comparer deux mondes. SEUL, et c'est la deuxième demande, celle qui compte. authorisedW, remainingW, binding, perPhaseBound sont ABSENTS, jamais à 0 — un authorisedW: 0 assorti d'un binding par défaut afficherait un plafond de soutirage qui n'existe pas, avec sa source. Un chiffre crédible et faux, c'est-à-dire la faute même que l'objet doit rendre impossible. budget.evReservedW n'a plus qu'UNE nature. Il recevait la part réseau d'EV_GRID_START — une vraie tranche d'allocation — en plus de la correction A. Déménagée dans draw.committedW. Sans ce déplacement, aucune définition du champ n'aurait été vraie pour ses deux termes, et l'app aurait renommé son libellé sur une définition à moitié fausse. Sa définition est écrite, puisqu'elle affichait « Réservé au véhicule » : c'est une CORRECTION du budget de surplus, pas une réservation ; elle tend vers 0 à mesure que la mesure rattrape la commande, sans qu'aucune décision n'ait changé. Le contrat dit : ne l'additionner à rien, il ne se réconcilie jamais. Le libellé affirme deux choses fausses — que c'est réservé, et que ça concerne le véhicule plutôt que la latence de sa mesure. EV_GRID_START cesse d'être une exception : ses deux moitiés tombent dans des compteurs de même nature. Le partage demeure — c'est un fait, une part est achetée — mais il ne s'énonce plus comme une entorse. Publication seule, aucune arithmétique de budget ne change : vérifié avant d'écrire que la ligne alimentant evReservedW depuis la cascade n'avait aucun retour dans le calcul. simulation 33/33, charging 16/16, loadmodel 20/20, amd64 0/0, doxygen 0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p --- INTERFACE.md | 66 ++++++++++++++++--- docs/BRIEF_depuis_plugin.md | 43 +++++++++++- energyplugin/etm/energyarbitrator.cpp | 13 ++++ .../etm/scheduler/rulebasedscheduler.cpp | 29 ++++++-- .../etm/scheduler/rulebasedscheduler.h | 5 +- energyplugin/etm/types/plan.h | 43 +++++++++++- energyplugin/nymeaenergyjsonhandler.cpp | 6 ++ tests/auto/simulation/simulation.cpp | 14 +++- 8 files changed, 200 insertions(+), 19 deletions(-) diff --git a/INTERFACE.md b/INTERFACE.md index 96bde4c..baf751e 100644 --- a/INTERFACE.md +++ b/INTERFACE.md @@ -593,6 +593,54 @@ le cas normal. > niveau il parle (`decision.level`, R7). On conserve un champ hérité **quand on peut en dire > quelque chose de vrai**, jamais par symétrie. +### `draw` — le registre des watts ACHETÉS + +Publié **dans la même trame que `budget`** : les deux décrivent le même cycle, et les lire à deux +instants ferait comparer deux mondes. + +```jsonc +"draw": { "committedW": 1400 } // watts achetés au réseau ce cycle +``` + +**Un seul champ, et c'est délibéré.** `authorisedW`, `remainingW`, `binding` et `perPhaseBound` +viendront avec le transport du §14a : **absents, jamais à `0`**. Un `authorisedW: 0` assorti d'un +`binding` par défaut ferait afficher un plafond de soutirage **qui n'existe pas, avec sa source** +— un chiffre crédible et faux, c'est-à-dire la faute même que cet objet doit rendre impossible. + +> **Ce qu'il compte** : ce que le moteur a **décidé d'acheter** selon son modèle de financement, +> pas ce que le compteur verra passer. Pour une action du proxy — échéance, tarif dynamique — +> **toute** l'allocation est comptée comme achetée : l'échéance décide de charger qu'il y ait du +> soleil ou non, et aucun partage n'est calculé. Là où le moteur **sait** partager, il partage : +> `EV_GRID_START` ne verse ici que sa part réseau, sa part surplus allant dans +> `budget.allocatedW`. +> +> **Omis en mode dégradé**, comme `budget` : sans plan, il n'y a pas de soutirage décidé. + +### ⚠️ `budget.evReservedW` n'est PAS un registre — et ne se réconcilie jamais + +*(Définition demandée par l'app le 2026-08-30, qui l'affichait sous « Réservé au véhicule » — un +libellé qui affirme une réservation là où il y a une correction.)* + +**`evReservedW` est une CORRECTION du budget de surplus.** Il vaut la somme des puissances +**commandées mais pas encore visibles au compteur** pour les charges servies au réseau. Il **tend +vers 0** à mesure que la mesure rattrape la commande, **sans qu'aucune décision n'ait changé** : +c'est un rattrapage de **latence**, pas un montant mis de côté. + +| | `budget.allocatedW` | `budget.evReservedW` | `draw.committedW` | +|---|---|---|---| +| Nature | registre d'allocation (surplus) | **correction** de budget | registre des watts achetés | +| Réconciliable | oui | **non, et jamais** | oui | +| Varie sans décision | non | **oui** — décroît quand la mesure rattrape | non | + +> **Ne l'additionnez à rien.** Le sommer avec des allocations compare deux natures, et le total +> obtenu serait **vrai au premier cycle après un changement, faux après convergence** — donc +> juste dans un test portant sur un cycle isolé, et faux chez le client. Une identité dépendante +> du temps est **pire que pas d'identité** : elle porte le sceau d'une identité publiée. +> +> **Depuis `1.15.2+etm31` il n'a plus qu'UNE nature.** Il recevait aussi la part réseau +> d'`EV_GRID_START` — une vraie tranche d'allocation — ce qui rendait toute définition du champ +> fausse pour l'un des deux termes. Cette part est passée dans `draw.committedW`. + ### Les TROIS nombres d'une charge — décidé, appliqué, mesuré *(Rendu explicite le 2026-08-30, après qu'une prémisse de la maquette app eut confondu les deux @@ -638,16 +686,16 @@ valait `"surplus"`, elle porte maintenant sur les **niveaux**. Un client qui la depuis l'ancienne forme divergerait **au premier cas mixte** — c'est-à-dire au premier cycle du §10. -> **L'exception, une seule, et elle est publiée** : `EV_GRID_START`. Ce motif partage **une** -> allocation entre **deux** compteurs — `params.budgetW` va dans `budget.allocatedW`, -> `params.gridW` dans `budget.evReservedW`, et `budgetW + gridW == allocatedW` toujours. Le -> niveau porte alors `funding: "grid"` : pour l'identité ci-dessus, sa contribution au surplus -> n'est **pas** `targetW` mais `decision.params.budgetW`. +> **`EV_GRID_START` n'est plus une exception depuis `+etm31`.** Ce motif partage **une** +> allocation entre deux registres, et les deux sont désormais des registres : `params.budgetW` +> va dans `budget.allocatedW`, `params.gridW` dans **`draw.committedW`** — et +> `budgetW + gridW == allocatedW` toujours. Sa contribution au surplus reste +> `decision.params.budgetW`, pas `targetW`. > -> C'est le seul endroit où un watt publié ne tombe pas dans un compteur unique, et c'est -> précisément ce que **R3** demande de généraliser — un mapping `(niveau, financement) → -> compteur` publié, plutôt qu'une exception à connaître. Tant que R3 n'est pas livrée, -> **l'exception est ici**, pas dans la tête du lecteur. +> Le partage lui-même ne disparaîtra pas — c'est un fait, une part est achetée et l'autre non. +> Ce qui disparaît est l'**exception** : les deux moitiés tombent maintenant dans des compteurs +> de même nature. `counts` (**R3**) publiera ce partage par niveau plutôt que de le laisser +> déduire du motif. > > `budget.evReservedW` **subsiste**, mais restreint : il ne compte plus que les bornes servies > par un financement **réseau** (échéance de départ, spot market), qui restent décidées en diff --git a/docs/BRIEF_depuis_plugin.md b/docs/BRIEF_depuis_plugin.md index 2d8fa4e..83bcaa2 100644 --- a/docs/BRIEF_depuis_plugin.md +++ b/docs/BRIEF_depuis_plugin.md @@ -774,7 +774,48 @@ correction, pas un compteur. | `budget.evReservedW` | **cesse d'être une destination** — redevient une correction du budget, et le contrat le dira | Avec cette forme, `Σ counts == targetW` tient **sans exception**, `EV_GRID_START` comprise -(900 → `allocatedW`, 480 → `draw.committedW`). Dites-nous, et nous l'écrivons dans la foulée. +(900 → `allocatedW`, 480 → `draw.committedW`). + +### ✅ LIVRÉ — `draw.committedW`, seul, `1.15.2+etm31` + +```jsonc +"budget": { … }, +"draw": { "committedW": 480 } // même trame que budget +``` + +**Vos trois demandes sont tenues.** + +**1. Par cycle, en watts, même trame que `budget`.** Publié dans le même instantané, jamais +séparément — sinon l'identité redeviendrait temporelle par un autre chemin. + +**2. Seul.** `authorisedW`, `remainingW`, `binding`, `perPhaseBound` : **absents**, pas à zéro. +Votre argument est dans le code et dans le contrat, en toutes lettres — un `authorisedW: 0` +assorti d'un `binding` par défaut afficherait un plafond qui n'existe pas, avec sa source. + +**3. La définition d'`evReservedW`**, et elle justifie de changer votre libellé : + +> **`evReservedW` est une CORRECTION du budget de surplus, pas une réservation.** Somme des +> puissances **commandées mais pas encore visibles au compteur** pour les charges servies au +> réseau. Elle **tend vers 0** à mesure que la mesure rattrape la commande, **sans qu'aucune +> décision n'ait changé** : c'est un rattrapage de **latence**, pas un montant mis de côté. + +« Réservé au véhicule » affirme donc deux choses fausses : que c'est réservé, et que ça concerne +le véhicule plutôt que la latence de sa mesure. Quelque chose comme « commandé, pas encore +mesuré » dirait ce que c'est — mais c'est votre écran. + +> **Et il n'a plus qu'une nature depuis `+etm31`.** Il recevait aussi la part réseau +> d'`EV_GRID_START`, une vraie tranche d'allocation ; nous l'avons déplacée dans +> `draw.committedW`. Sans ce déménagement, aucune définition du champ n'aurait été vraie pour +> ses deux termes — et vous auriez renommé un libellé sur une définition à moitié fausse. + +`EV_GRID_START` **cesse d'être une exception** : ses deux moitiés tombent désormais dans des +compteurs de **même nature** (900 → `budget.allocatedW`, 480 → `draw.committedW`). Le partage +demeure — c'est un fait — mais il ne s'énonce plus comme une entorse. **Votre sonde peut aller le +voir sur `.75` dès que nous aurons posé `+etm31`.** + +**`counts` suit maintenant**, dans la forme arbitrée : clés opaques jamais recyclées, publié même +à zéro avec sa destination, et faisant foi pour la comptabilité — les `params` gardant la phrase +du motif. > Et deux fils antérieurs de votre brief sont **clos**, au cas où ils ne vous seraient pas > parvenus : l'**entrelacement des domaines** est tranché — grouper à l'affichage, **jamais à diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp index 11c0ae1..f7dc789 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -1023,6 +1023,19 @@ void EnergyArbitrator::buildTelemetry(const Plan &plan, const Slot &slot, const budget.insert(QStringLiteral("remainingW"), plan.budget.remainingW); snap.insert(QStringLiteral("budget"), budget); + // R3 — LE SOUTIRAGE, dans la MÊME TRAME que le budget. Les deux décrivent le même cycle ; + // les publier séparément ferait comparer deux instants, et l'identité de réconciliation + // redeviendrait temporelle par un autre chemin — ce que R3 existe pour empêcher. + // + // UN SEUL CHAMP, et c'est délibéré. authorisedW, remainingW, binding et perPhaseBound + // viendront avec le transport du §14a : absents, JAMAIS à 0. Un authorisedW: 0 assorti d'un + // binding par défaut ferait afficher un plafond de soutirage qui n'existe pas, avec sa + // source — un chiffre crédible et faux, c'est-à-dire la faute même que cet objet doit + // rendre impossible. + QVariantMap draw; + draw.insert(QStringLiteral("committedW"), plan.draw.committedW); + snap.insert(QStringLiteral("draw"), draw); + // On itère les ACTIONS, pas les tables d'adaptateurs : l'ordre du waterfall (rang 1 servi en // premier) est porté par le slot, et QHash n'en garantit aucun. Un ordre instable ferait // aussi diverger la comparaison de seuil d'un cycle à l'autre, pour rien. diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.cpp b/energyplugin/etm/scheduler/rulebasedscheduler.cpp index b448208..93b805b 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.cpp +++ b/energyplugin/etm/scheduler/rulebasedscheduler.cpp @@ -88,6 +88,7 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) // (invariant 8). Les consignes EV de CE cycle ne sont pas encore visibles au // compteur ; on réserve donc leur puissance commandée non encore mesurée. double evReservedW = 0; + double drawCommittedW = 0; // R3 — watts ACHETÉS ce cycle for (const LoadAction &la : slot.actions) { if (la.kind != LoadAction::Setpoint || !la.chargingEnabled) continue; @@ -99,6 +100,16 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) EvCharger *ev = evs.value(ThingId(la.loadId)); const double measuredW = ev ? ev->currentPower() : 0.0; evReservedW += qMax(0.0, la.estimatedPowerW - measuredW); + // R3 — LE REGISTRE DES WATTS ACHETÉS. Distinct de la correction ci-dessus, et la + // distinction est la découverte du lot : evReservedW décroît à mesure que la mesure + // rattrape la commande, alors que ce qui a été ACHETÉ ne bouge pas. Confondre les deux + // donnerait une identité vraie au premier cycle et fausse après convergence — qui + // passerait tous les tests, puisqu'ils portent sur des cycles isolés. + // + // Toute l'allocation du proxy y entre : l'échéance décide de charger qu'il y ait du + // soleil ou non, et le moteur ne calcule aucun partage pour elle. Là où il SAIT + // partager, il partage — voir EV_GRID_START. + drawCommittedW += la.estimatedPowerW; } // Surplus net SIGNÉ : exportW − importW = −puissance compteur. Négatif en import → // l'ECS déleste (budget < palier) au lieu de rester allumé sur le réseau. (Le maintien @@ -161,6 +172,8 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) // été alloué et ce qui a été recrédité. La recalculer ailleurs donnerait deux comptes à // tenir d'accord, et le chiffre publié serait le premier à mentir. PlanBudget compta; + PlanDraw draw; // registre des watts achetés — R3 + draw.committedW = drawCommittedW; // ce que le proxy a déjà engagé au réseau compta.surplusW = remainingSurplusW; compta.evReservedW = evReservedW; @@ -208,7 +221,7 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) if (lc.adapter == QStringLiteral("sg-ready")) slot.actions.append(buildSgReadyStateAction(lc, remainingSurplusW, compta)); else - slot.actions.append(buildSetpointAction(lc, remainingSurplusW, compta)); // etmvariableload + relay-router + evcharger + slot.actions.append(buildSetpointAction(lc, remainingSurplusW, compta, draw)); // etmvariableload + relay-router + evcharger // 3g-2 — c'est ICI que le jeu se constitue, au moment où la décision est prise. Le // redéduire plus tard depuis les motifs ou le financement dériverait du vrai au // premier motif ajouté (EV_GRID_START porte funding=Grid tout en venant du waterfall). @@ -225,6 +238,9 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) compta.remainingW = remainingSurplusW; plan.budget = compta; + // R3 — le soutirage est publié DANS LA MÊME TRAME que le budget : les deux décrivent le + // même cycle, et les lire à deux instants ferait comparer deux mondes. + plan.draw = draw; plan.waterfallEvIds = waterfallEvIds; return plan; } @@ -248,7 +264,7 @@ LoadAction RuleBasedScheduler::buildTimeRequirementAction(EvCharger *ev, LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc, double &remainingSurplusW, - PlanBudget &budget) const + PlanBudget &budget, PlanDraw &draw) const { // Recrédit anti-clignotement (Correction B / contrat rév. 2 §5) : la conso de DÉBUT de cycle // est déjà soustraite de l'export mesuré → on la rend au budget local avant d'arrondir. @@ -351,8 +367,13 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc, // réseau et se compte là où vivent déjà les puissances commandées non encore // mesurées. Laisser le budget filer en négatif ferait porter le soutirage par // la cascade, où il se lirait comme un manque de surplus chez les suivantes. - budget.allocatedW += budgetW; - budget.evReservedW += soutireW; + // R3 — le moteur SAIT partager ici, donc il partage : la part surplus entre + // dans le registre d'allocation, la part achetée dans le registre de soutirage. + // Elle allait dans evReservedW, qui est une CORRECTION : une tranche + // d'allocation y rendait toute définition du champ fausse pour l'un des deux + // termes, et interdisait de le nommer honnêtement à l'écran. + budget.allocatedW += budgetW; + draw.committedW += soutireW; remainingSurplusW = 0; return la; } diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.h b/energyplugin/etm/scheduler/rulebasedscheduler.h index aa3d25a..6fbe6f5 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.h +++ b/energyplugin/etm/scheduler/rulebasedscheduler.h @@ -89,8 +89,11 @@ private: * qu'une charge consomme déjà — c'est-à-dire dans le cas normal. * \return LoadAction kind=Setpoint, funding=Surplus, \c powerW = consigne, \c reason non vide. */ + //! \param draw Registre des watts ACHETÉS (R3) — alimenté par EV_GRID_START, qui est le + //! seul endroit de la cascade où le moteur sache partager une allocation entre le + //! surplus et le réseau. LoadAction buildSetpointAction(const LoadContext &lc, double &remainingSurplusW, - PlanBudget &budget) const; + PlanBudget &budget, PlanDraw &draw) const; /*! * \brief Construit un LoadAction "state" SG-Ready (PAC) par mapping SÉMANTIQUE du surplus. diff --git a/energyplugin/etm/types/plan.h b/energyplugin/etm/types/plan.h index a0f5ec3..2ae0cdd 100644 --- a/energyplugin/etm/types/plan.h +++ b/energyplugin/etm/types/plan.h @@ -50,9 +50,21 @@ struct PlanBudget { //! Budget entrant du waterfall non-EV : surplus net signé (export − import) MOINS la //! réservation EV du cycle. Négatif = soutirage réseau, le waterfall déleste. double surplusW = 0; - //! Puissance réservée aux consignes EV de CE cycle, pas encore visible au compteur - //! (correction A). Publiée pour que \c surplusW reste rapprochable de la mesure brute : - //! sans elle, l'écart avec le compteur serait inexplicable côté client. + /*! + * \brief CORRECTION du budget de surplus — **jamais une réservation**, malgré son nom. + * + * Somme des puissances **commandées mais pas encore visibles au compteur** pour les charges + * servies au réseau (correction A). Elle **tend vers 0** à mesure que la mesure rattrape la + * commande, sans qu'aucune décision n'ait changé : c'est un rattrapage de **latence**, pas + * un montant mis de côté. + * + * \warning **Ce champ n'est pas réconciliable, et il ne le sera jamais** — ce n'est pas un + * registre. Additionner des allocations avec lui compare deux natures. Les watts + * **achetés** ont leur registre : \c PlanDraw::committedW. + * \note Depuis `1.15.2+etm31` il n'a plus qu'UNE nature. Il recevait aussi la part réseau + * d'`EV_GRID_START` — une vraie tranche d'allocation — ce qui rendait toute définition du + * champ fausse pour l'un des deux termes. Cette part est passée dans \c PlanDraw. + */ double evReservedW = 0; //! Somme des puissances ALLOUÉES aux charges non-EV par le waterfall de ce cycle — le //! total, pas l'incrément. C'est la somme des \c estimatedPowerW des actions du créneau. @@ -64,6 +76,29 @@ struct PlanBudget { double remainingW = 0; }; +/*! + * \brief Le SOUTIRAGE du cycle — registre des watts **achetés au réseau**. + * + * Publié à côté de \c budget, dans la **même trame** : les deux décrivent le même cycle, et les + * lire à deux instants ferait comparer deux mondes. + * + * \note **Ce que le moteur a décidé d'acheter selon son modèle de financement**, pas ce que le + * compteur verra passer. Pour une action du proxy — échéance, tarif dynamique — le modèle + * traite **toute** l'allocation comme achetée : l'échéance décide de charger qu'il y ait du + * soleil ou non, et aucun partage n'est calculé. Là où le moteur SAIT partager, il partage : + * `EV_GRID_START` ne verse ici que sa part réseau, sa part surplus allant dans + * \c PlanBudget::allocatedW. + * \note **Un seul champ pour l'instant, et c'est délibéré.** `authorisedW`, `remainingW`, + * `binding` et `perPhaseBound` viendront avec le transport du §14a — absents, **jamais à 0** : + * un `authorisedW: 0` assorti d'un `binding` par défaut ferait afficher un plafond de + * soutirage qui n'existe pas, avec sa source. Un chiffre crédible et faux, c'est-à-dire la + * faute même que cet objet doit rendre impossible. + */ +struct PlanDraw { + //! Watts achetés au réseau ce cycle. Toujours ≥ 0. + double committedW = 0; +}; + /*! * \brief Plan d'optimisation retourné par IScheduler::getPlan(). * @@ -80,6 +115,8 @@ struct Plan { //! Comptabilité du budget du cycle — INTERNE, jamais sérialisée vers l'optimiseur socket //! (même statut que \c LoadAction::funding). Alimente la télémétrie RPC. PlanBudget budget; + //! Soutirage du cycle — registre des watts achetés (\c PlanDraw). + PlanDraw draw; /*! * \brief [3g-2] Bornes VE dont le waterfall a pris la décision ce cycle — INTERNE. * diff --git a/energyplugin/nymeaenergyjsonhandler.cpp b/energyplugin/nymeaenergyjsonhandler.cpp index 1396594..659aab3 100644 --- a/energyplugin/nymeaenergyjsonhandler.cpp +++ b/energyplugin/nymeaenergyjsonhandler.cpp @@ -821,6 +821,12 @@ QVariantMap NymeaEnergyJsonHandler::loadTelemetrySchema() schema.insert("o:timestamp", enumValueName(String)); // ISO 8601 UTC schema.insert("degradedMode", enumValueName(Bool)); schema.insert("o:budget", budgetItem); + // R3 — registre des watts ACHETÉS, dans la même trame que « budget ». Un seul champ + // aujourd'hui : les quatre autres du §14a sont ABSENTS, jamais à 0 (voir buildTelemetry). + // Omis en mode dégradé, comme « budget » : sans plan, il n'y a pas de soutirage décidé. + QVariantMap drawItem; + drawItem.insert("committedW", enumValueName(Double)); + schema.insert("o:draw", drawItem); schema.insert("loads", QVariantList() << loadItem); return schema; } diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index 5f15928..4caaa0f 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -5345,8 +5345,20 @@ void Simulation::testEvGridStartIsAnnouncedAndBounded() // ---- La comptabilité du budget partage l'allocation ------------------------------------ const QVariantMap budget = snap.value("budget").toMap(); QCOMPARE(qRound(budget.value("allocatedW").toDouble()), 900); // la part de SURPLUS - QCOMPARE(qRound(budget.value("evReservedW").toDouble()), 480); // la part de RÉSEAU QCOMPARE(qRound(budget.value("remainingW").toDouble()), 0); + + // R3 — la part ACHETÉE a quitté evReservedW pour le registre du soutirage. evReservedW est + // une CORRECTION du budget (commandé non encore mesuré) : y verser une tranche + // d'allocation rendait toute définition du champ fausse pour l'un des deux termes, et + // interdisait de le nommer honnêtement à l'écran. + QCOMPARE(qRound(snap.value("draw").toMap().value("committedW").toDouble()), 480); + QCOMPARE(qRound(budget.value("evReservedW").toDouble()), 0); + + // budgetW + gridW == allocatedW tient toujours, et se lit désormais SANS exception : + // la part surplus dans le registre d'allocation, la part achetée dans celui du soutirage. + QCOMPARE(qRound(budget.value("allocatedW").toDouble() + + snap.value("draw").toMap().value("committedW").toDouble()), + qRound(tele.value(borneAId.toString()).value("allocatedW").toDouble())); #endif }