Compare commits
2 Commits
86256d0baf
...
fa3c8d4755
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa3c8d4755 | ||
|
|
0908943da8 |
159
docs/DESIGN_DELESTAGE.md
Normal file
159
docs/DESIGN_DELESTAGE.md
Normal file
@ -0,0 +1,159 @@
|
||||
# DESIGN — l'extension du délestage au waterfall
|
||||
|
||||
**Statut : conception, à valider avant tout code de liaison** (`AGENTS.md`, workflow obligatoire).
|
||||
**Précondition de** : `§10` (LM-1006, couplage 1). Le §10 ne part pas sans elle.
|
||||
**Contrainte de forme** : `specs/spec_loadmodel.md` **LM-1006-1**, écrite le 2026-08-27
|
||||
*« pour que la porte reste ouverte »*. Elle se referme ici.
|
||||
|
||||
> **Ce document ne demande pas si le délestage doit s'étendre.** Il dit sous quelle forme, dans
|
||||
> quel ordre, et nomme **ce qui demande une décision** — §6.
|
||||
|
||||
---
|
||||
|
||||
## 1. L'écart, mesuré
|
||||
|
||||
| | Aujourd'hui |
|
||||
|---|---|
|
||||
| **Qui est délesté** | `verifyOverloadProtection()` parcourt `m_evChargers` et **rien d'autre** |
|
||||
| **Ce que le waterfall sait du plafond** | `evPhaseAllowanceW()` — et **uniquement pour les bornes**, comme plafond déclaré d'une borne |
|
||||
| **Charges pilotées, PAC** | aucun plafond : leur `maxPowerW` vient de la configuration, la cascade n'est bornée que par le **surplus** |
|
||||
|
||||
Tant que la cascade ne dépense que du surplus, l'absence de plafond ne se voit pas : on ne peut
|
||||
pas dépasser ce qu'on n'achète pas. **Le §10 change cela** — la passe éco décide d'acheter, et
|
||||
plusieurs planchers qui s'ouvrent le même matin sans soleil s'additionnent sur le disjoncteur de
|
||||
branchement.
|
||||
|
||||
---
|
||||
|
||||
## 2. Le mécanisme : UN plafond, TROIS sources
|
||||
|
||||
LM-1006-1 l'impose et c'est le cœur du lot : **la source est un paramètre, jamais une hypothèse
|
||||
enfouie dans le nom de la fonction.**
|
||||
|
||||
```
|
||||
struct DrawCap {
|
||||
Source source; // Breaker | GridOperator | EcoSelfImposed
|
||||
double perPhaseW; // < 0 = ce plafond ne borne pas par phase
|
||||
double totalW; // < 0 = ce plafond ne borne pas au total
|
||||
};
|
||||
```
|
||||
|
||||
**Les deux formes coexistent et ne se convertissent JAMAIS** (LM-1006-1). Un plafond par phase
|
||||
n'est pas un tiers d'un plafond total : une maison peut être à 20 A sur une phase et 2 A sur les
|
||||
deux autres.
|
||||
|
||||
| Source | Forme | Durée | Origine |
|
||||
|---|---|---|---|
|
||||
| `Breaker` | **par phase** | permanent | `phasePowerLimit`, réglage d'installation |
|
||||
| `GridOperator` | **total** | temporaire, sur ordre | §14a — *le transport n'existe pas* |
|
||||
| `EcoSelfImposed` | **total** | le temps de la passe 1 | §10 — *la passe n'existe pas* |
|
||||
|
||||
> **Deux des trois sources n'ont pas encore d'émetteur, et c'est voulu.** Le mécanisme se conçoit
|
||||
> pour trois parce que l'écrire pour une le rendrait irréversible : *« cela tient dans une
|
||||
> signature aujourd'hui ; ce serait une refonte dans deux ans »* (LM-1006-1).
|
||||
|
||||
---
|
||||
|
||||
## 3. Comment on compare sans convertir
|
||||
|
||||
C'est le point technique du lot, et il n'est pas évident. On ne compare pas les **plafonds** —
|
||||
ils n'ont pas la même forme. **On compare les MARGES qu'ils laissent.**
|
||||
|
||||
```
|
||||
marge(cap) = combien de watts SUPPLÉMENTAIRES ce plafond autorise, ici et maintenant
|
||||
```
|
||||
|
||||
- pour un plafond **par phase** : la marge est celle de la **phase la plus chargée**, ramenée en
|
||||
watts — c'est déjà ce que fait `RootMeter::calculateAllowanceAmpere()` ;
|
||||
- pour un plafond **total** : la marge est `totalW − soutirage mesuré`.
|
||||
|
||||
La marge est un **nombre de watts** dans les deux cas : elle se compare. **Le plafond qui laisse
|
||||
la plus petite marge MORD**, et c'est sa `source` qu'on retient.
|
||||
|
||||
> **Ce que cela préserve** : aucune conversion, donc aucune hypothèse d'équilibre entre phases.
|
||||
> Un plafond par phase reste évalué phase par phase jusqu'au bout ; il ne devient jamais un
|
||||
> « total équivalent ».
|
||||
>
|
||||
> **Et ce que ça donne gratuitement** : la marge est exactement ce que la cascade doit connaître
|
||||
> pour ne pas dépasser. Le même nombre sert à borner et à expliquer.
|
||||
|
||||
---
|
||||
|
||||
## 4. Où le plafond mord — deux endroits, deux rôles
|
||||
|
||||
Il faut les distinguer, sinon on écrit deux fois la même chose ou on en oublie une.
|
||||
|
||||
| | Quand | Ce qu'il fait |
|
||||
|---|---|---|
|
||||
| **Dans la cascade** (L3, décision) | à chaque cycle, **avant** de commander | borne l'allocation : on ne décide pas d'acheter au-delà de la marge |
|
||||
| **Dans la protection** (L4, réaction) | sur `powerBalanceChanged`, **après** la mesure | déleste ce qui dépasse déjà, dans l'**ordre inverse du rang** |
|
||||
|
||||
**Les deux sont nécessaires, et ce n'est pas une redondance** : la première empêche de créer le
|
||||
dépassement, la seconde rattrape ce qu'on n'a pas décidé — une plaque de cuisson, un
|
||||
sèche-linge, tout ce que le HEMS ne pilote pas. La couche L4 garde le dernier mot
|
||||
(`AGENTS.md`, MODÈLE DE SÉCURITÉ), et ce lot ne change pas cela.
|
||||
|
||||
> **L'ordre de coupe est l'inverse du rang** : la charge servie en dernier est délestée en
|
||||
> premier. C'est le seul ordre qui respecte le classement de l'installateur — l'inverse
|
||||
> reviendrait à le contredire au moment où il compte le plus.
|
||||
|
||||
---
|
||||
|
||||
## 5. Ordre de travail — quatre étapes, dont deux à comportement constant
|
||||
|
||||
**Étape 1 — le TYPE et sa résolution.** `DrawCap`, la marge, « le plus contraignant l'emporte et
|
||||
on sait lequel ». Testable **isolément** (`tests/auto/loadmodel`, sans serveur nymea).
|
||||
**Comportement inchangé** : rien ne le consomme encore.
|
||||
|
||||
**Étape 2 — la SOURCE `Breaker` branchée, à comportement CONSTANT.** `phasePowerLimit` devient un
|
||||
`DrawCap{Breaker, perPhaseW}` et `evPhaseAllowanceW()` passe par la résolution. Le résultat doit
|
||||
être **identique au watt près** — c'est ce qui rend l'étape vérifiable : les 46 tests de
|
||||
`charging` ne doivent pas bouger d'une ligne.
|
||||
|
||||
**Étape 3 — la cascade est bornée par la marge.** Le waterfall cesse d'allouer au-delà. C'est ici
|
||||
que le comportement change pour les charges non-EV, et c'est ici que le motif `DRAW_CAP` naît
|
||||
(**R8** : il publie sa source).
|
||||
|
||||
**Étape 4 — la protection L4 déleste le waterfall.** `verifyOverloadProtection()` cesse d'être
|
||||
réservée aux bornes ; ordre inverse du rang, `force = true`, verrous bypassés comme le repli L2.
|
||||
|
||||
> **Les deux premières ne changent rien et rendent les deux suivantes petites.** C'est la même
|
||||
> stratégie que le §10 : ce qui coûte, dans un lot de sécurité, n'est pas d'écrire le
|
||||
> comportement — c'est de prouver qu'on n'a rien cassé en chemin.
|
||||
|
||||
---
|
||||
|
||||
## 6. CE QUI DEMANDE UNE DÉCISION
|
||||
|
||||
**6.1 — La marge doit-elle tenir compte de ce que le HEMS a lui-même commandé et qui n'est pas
|
||||
encore mesuré ?** C'est la correction A, transposée au soutirage. Sans elle, deux cycles
|
||||
consécutifs peuvent décider d'acheter la même marge deux fois. Avec elle, on réintroduit un
|
||||
terme de latence dans une couche de **sécurité**, où une hypothèse fausse coupe du courant.
|
||||
**Recommandation : NON dans la cascade, OUI nulle part ailleurs** — la cascade borne sur la marge
|
||||
MESURÉE, et l'écart d'un cycle est absorbé par la marge que le disjoncteur garde par
|
||||
construction (`allowancePerPhase -= 1` existe déjà côté proxy). À trancher explicitement.
|
||||
|
||||
**6.2 — Que fait le délestage L4 d'une charge sous verrou `minOn` ?** Le repli L2 bypasse les
|
||||
verrous (`force = true`) parce qu'un compteur muet est une panne. Un dépassement de plafond
|
||||
est-il de la même gravité ? **Recommandation : OUI pour `Breaker` et `GridOperator`** — le
|
||||
disjoncteur ne connaît pas le court-cycling, et un ordre du gestionnaire ne se négocie pas.
|
||||
**NON pour `EcoSelfImposed`** : un plafond qu'on s'impose ne justifie pas d'abîmer un
|
||||
compresseur. C'est la première conséquence concrète du fait que la source soit un paramètre.
|
||||
|
||||
**6.3 — Le motif `DRAW_CAP` naît à l'étape 3 : que porte-t-il exactement ?** R8 demande la
|
||||
source. Faut-il aussi `capW`, `marginW`, et la **phase** qui borne quand la source est
|
||||
`Breaker` ? **Recommandation : oui aux trois** — sans la phase, un installateur devant une maison
|
||||
triphasée déséquilibrée ne sait pas où aller mesurer.
|
||||
|
||||
---
|
||||
|
||||
## 7. Ce que ce lot ne fait PAS
|
||||
|
||||
- **Il ne crée aucun transport §14a.** `GridOperator` sera une source qu'aucun émetteur ne
|
||||
remplit ; c'est le but — la porte reste ouverte, elle ne s'ouvre pas.
|
||||
- **Il ne touche pas au repli L2.** LM-1006-1, propriété 2 : le plafond **auto-imposé** est
|
||||
respecté par construction dès lors que le repli **ne dépense pas**. L'exigence dure — tenir
|
||||
une borne imposée par un tiers alors que le moteur ne décide plus — appartient au **§14a** et
|
||||
vivra **hors du moteur**. Les confondre ferait hériter à ce lot d'une contrainte qui ne le
|
||||
concerne pas.
|
||||
- **Il ne déplace pas la frontière de sécurité.** L4 garde le dernier mot, L0 reste physique.
|
||||
@ -4,6 +4,7 @@ HEADERS += \
|
||||
$$PWD/types/loaddescriptor.h \
|
||||
$$PWD/types/surpluscontext.h \
|
||||
$$PWD/types/plan.h \
|
||||
$$PWD/types/drawcap.h \
|
||||
$$PWD/types/loadconfig.h \
|
||||
$$PWD/config/loadconfigstore.h \
|
||||
$$PWD/adapters/iloadadapter.h \
|
||||
@ -18,6 +19,7 @@ HEADERS += \
|
||||
|
||||
SOURCES += \
|
||||
$$PWD/types/decisionreason.cpp \
|
||||
$$PWD/types/drawcap.cpp \
|
||||
$$PWD/types/loadconfig.cpp \
|
||||
$$PWD/config/loadconfigstore.cpp \
|
||||
$$PWD/ratios/energyratioscalculator.cpp \
|
||||
|
||||
66
energyplugin/etm/types/drawcap.cpp
Normal file
66
energyplugin/etm/types/drawcap.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
|
||||
#include "drawcap.h"
|
||||
|
||||
namespace {
|
||||
//! Rang de contrainte d'une source, pour départager DEUX MARGES ÉGALES. Plus haut = plus
|
||||
//! contraignant. Ce n'est pas une hiérarchie de sévérité — c'est un départage STABLE : sans
|
||||
//! lui, le motif publié dépendrait de l'ordre du tableau de plafonds, et changerait sans
|
||||
//! qu'aucune décision n'ait changé.
|
||||
int rangDeContrainte(DrawCapSource s)
|
||||
{
|
||||
switch (s) {
|
||||
case DrawCapSource::Breaker: return 3; // physique : rien ne passe outre
|
||||
case DrawCapSource::GridOperator: return 2; // imposé : ne se négocie pas
|
||||
case DrawCapSource::EcoSelfImposed: return 1; // auto-imposé : se révise
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
DrawMargin resolveDrawMargin(const QList<DrawCap> &caps,
|
||||
const QList<double> &perPhaseW,
|
||||
double totalW)
|
||||
{
|
||||
DrawMargin retenue;
|
||||
if (caps.isEmpty())
|
||||
return retenue; // borne == false : personne ne borne, la marge est SANS OBJET
|
||||
|
||||
auto retenir = [&retenue](double marge, DrawCapSource src, int phase) {
|
||||
// Plus petite marge, puis départage par rang de contrainte à marge égale.
|
||||
if (!retenue.borne || marge < retenue.watts
|
||||
|| (qFuzzyCompare(marge + 1.0, retenue.watts + 1.0)
|
||||
&& rangDeContrainte(src) > rangDeContrainte(retenue.source))) {
|
||||
retenue.watts = marge;
|
||||
retenue.source = src;
|
||||
retenue.phase = phase;
|
||||
retenue.borne = true;
|
||||
}
|
||||
};
|
||||
|
||||
for (const DrawCap &cap : caps) {
|
||||
// Forme PAR PHASE : la marge est celle de la phase la plus chargée. On l'évalue phase
|
||||
// par phase jusqu'au bout — la ramener à un « total équivalent » supposerait un
|
||||
// équilibre que rien ne garantit (LM-1006-1).
|
||||
if (cap.bornePhase()) {
|
||||
for (int i = 0; i < perPhaseW.size(); ++i)
|
||||
retenir(cap.perPhaseW - perPhaseW.at(i), cap.source, i);
|
||||
}
|
||||
// Forme TOTALE : la marge est ce qui reste sous le plafond au point de livraison.
|
||||
// phase = -1 — il n'y en a pas, et prétendre le contraire enverrait mesurer au hasard.
|
||||
if (cap.borneTotal())
|
||||
retenir(cap.totalW - totalW, cap.source, -1);
|
||||
}
|
||||
return retenue;
|
||||
}
|
||||
|
||||
QString drawCapSourceCode(DrawCapSource s)
|
||||
{
|
||||
switch (s) {
|
||||
case DrawCapSource::Breaker: return QStringLiteral("connection");
|
||||
case DrawCapSource::GridOperator: return QStringLiteral("gridOperator");
|
||||
case DrawCapSource::EcoSelfImposed: return QStringLiteral("selfImposed");
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
107
energyplugin/etm/types/drawcap.h
Normal file
107
energyplugin/etm/types/drawcap.h
Normal file
@ -0,0 +1,107 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
||||
/*!
|
||||
* \file drawcap.h
|
||||
* \brief Le PLAFOND DE SOUTIRAGE et sa source — \c specs/spec_loadmodel.md LM-1006-1.
|
||||
*
|
||||
* \par Pourquoi ce type existe
|
||||
* Le délestage n'est pas « la protection de surcharge » : c'est **l'application d'un plafond
|
||||
* de soutirage, quelle qu'en soit la source**. Trois sources, un seul mécanisme — et la
|
||||
* source est un **paramètre**, jamais une hypothèse enfouie dans le nom d'une fonction.
|
||||
* « Cela tient dans une signature aujourd'hui ; ce serait une refonte dans deux ans. »
|
||||
*
|
||||
* \par Deux formes qui ne se convertissent JAMAIS
|
||||
* Un plafond **par phase** n'est pas le tiers d'un plafond **total** : une maison peut tirer
|
||||
* 20 A sur une phase et 2 A sur les deux autres. Convertir supposerait un équilibre que rien
|
||||
* ne garantit — et l'hypothèse serait invisible jusqu'au jour où elle coupe du courant.
|
||||
*/
|
||||
|
||||
//! \brief D'où vient un plafond de soutirage. La source décide du MOTIF publié et du GESTE
|
||||
//! qu'il appelle (LM-1006-1, propriété 1) — trois sources, trois phrases, trois gestes.
|
||||
enum class DrawCapSource {
|
||||
//! Disjoncteur de branchement — physique, permanent, **par phase**. Le geste : délester la
|
||||
//! maison, ou revoir l'abonnement.
|
||||
Breaker,
|
||||
//! §14a EnWG — externe, **sur ordre** du gestionnaire de réseau, temporaire, **total**. Le
|
||||
//! geste : se renseigner auprès du fournisseur ; cela se subit, cela ne s'attaque pas.
|
||||
//! \note Aucun émetteur ne le remplit aujourd'hui — le transport du §14a n'existe pas. La
|
||||
//! source est déclarée pour que la porte reste ouverte, pas parce qu'elle s'ouvre.
|
||||
GridOperator,
|
||||
//! §10 — celui qu'on **s'impose** en décidant de soutirer pour un plancher éco. Total, le
|
||||
//! temps de la passe 1. Le geste : revoir le réglage éco.
|
||||
//! \note Aucun émetteur non plus : la passe éco n'existe pas.
|
||||
EcoSelfImposed
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Un plafond de soutirage, dans la forme où sa source l'exprime.
|
||||
*
|
||||
* \invariant Une des deux formes AU MOINS est renseignée ; une valeur **négative** signifie
|
||||
* « ce plafond ne borne pas de cette façon », jamais « zéro watt autorisé ». Zéro est une
|
||||
* valeur licite et voudrait dire *interdiction totale de soutirer*.
|
||||
*/
|
||||
struct DrawCap {
|
||||
DrawCapSource source = DrawCapSource::Breaker;
|
||||
//! Plafond **par phase**, en watts. Négatif = ce plafond ne borne pas par phase.
|
||||
double perPhaseW = -1;
|
||||
//! Plafond **total** au point de livraison, en watts. Négatif = ne borne pas au total.
|
||||
double totalW = -1;
|
||||
|
||||
bool bornePhase() const { return perPhaseW >= 0; }
|
||||
bool borneTotal() const { return totalW >= 0; }
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Ce qu'un plafond autorise ENCORE, ici et maintenant — et lequel a mordu.
|
||||
*
|
||||
* \par Comparer des marges, jamais des plafonds
|
||||
* C'est le point technique de LM-1006-1 : deux plafonds de formes différentes ne se comparent
|
||||
* pas, mais les **marges** qu'ils laissent sont des watts dans les deux cas. Le plafond qui
|
||||
* laisse la plus petite marge **mord**, et c'est sa source qu'on retient. Aucune conversion,
|
||||
* donc aucune hypothèse d'équilibre entre phases.
|
||||
*/
|
||||
struct DrawMargin {
|
||||
//! Watts de soutirage SUPPLÉMENTAIRE autorisés. Peut être **négatif** : le plafond est déjà
|
||||
//! dépassé, et l'ampleur du dépassement est ce qu'il faut délester.
|
||||
double watts = 0;
|
||||
//! La source du plafond qui a mordu — celle qui appartient au motif publié (R8).
|
||||
DrawCapSource source = DrawCapSource::Breaker;
|
||||
//! Index de la phase qui borne (0=A, 1=B, 2=C), ou **-1** si c'est un plafond TOTAL qui
|
||||
//! mord. Sans elle, un installateur devant une maison triphasée déséquilibrée ne sait pas
|
||||
//! où aller mesurer.
|
||||
int phase = -1;
|
||||
//! Faux si AUCUN plafond n'était déclaré — la marge est alors sans objet, et le distinguer
|
||||
//! d'une marge nulle est ce qui empêche un « 0 W autorisé » inventé de couper des charges.
|
||||
bool borne = false;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Résout la marge à partir de plusieurs plafonds et de la mesure du cycle.
|
||||
*
|
||||
* \param caps Plafonds en vigueur, toutes sources confondues. Vide = rien ne borne.
|
||||
* \param perPhaseW Soutirage MESURÉ par phase (A, B, C), en watts. Négatif = export.
|
||||
* \param totalW Soutirage MESURÉ total, en watts. Négatif = export.
|
||||
* \return La plus petite marge, avec la source qui l'a produite. \c borne faux si \p caps est
|
||||
* vide — auquel cas \c watts n'a aucun sens et ne doit borner personne.
|
||||
*
|
||||
* \note **Le plus CONTRAIGNANT l'emporte, et on sait lequel.** C'est l'exigence 1 de LM-1006-1,
|
||||
* et elle est la raison d'être de cette fonction : un client bridé doit lire « gestionnaire de
|
||||
* réseau » et non « protection de surcharge ». Deux causes, deux gestes — l'une se subit et se
|
||||
* renseigne, l'autre s'attaque en délestant ou en revoyant l'abonnement.
|
||||
* \note **Égalité : la source la plus CONTRAIGNANTE par nature l'emporte**, dans l'ordre
|
||||
* \c Breaker > \c GridOperator > \c EcoSelfImposed. Non par prudence, mais parce qu'un
|
||||
* arbitrage stable vaut mieux qu'un arbitrage juste-mais-instable : à marges égales, le motif
|
||||
* publié ne doit pas dépendre de l'ordre du tableau.
|
||||
*/
|
||||
DrawMargin resolveDrawMargin(const QList<DrawCap> &caps,
|
||||
const QList<double> &perPhaseW,
|
||||
double totalW);
|
||||
|
||||
//! \brief Code de la source, tel qu'il franchit la frontière RPC. Énumération FERMÉE.
|
||||
//! \param s Source. \return \c "connection" · \c "gridOperator" · \c "selfImposed".
|
||||
QString drawCapSourceCode(DrawCapSource s);
|
||||
@ -5,6 +5,7 @@
|
||||
#include "etm/adapters/relayrouter.h"
|
||||
#include "etm/energyarbitrator.h"
|
||||
#include "etm/types/loadconfig.h"
|
||||
#include "etm/types/drawcap.h"
|
||||
#include "etm/config/loadconfigstore.h"
|
||||
|
||||
#include <QTest>
|
||||
@ -534,3 +535,63 @@ void TestLoadModel::testRankOriginFollowsTheRankNotTheWrite()
|
||||
hors.setRankOrigin(QStringLiteral("installateur"));
|
||||
QVERIFY2(!hors.isValid(&err), "rankOrigin hors énumération doit être refusé");
|
||||
}
|
||||
|
||||
void TestLoadModel::testDrawMarginComparesHeadroomsNotCaps()
|
||||
{
|
||||
// [LM-1006-1] Un plafond PAR PHASE et un plafond TOTAL n'ont pas la même forme et ne se
|
||||
// convertissent pas : une maison peut tirer 20 A sur une phase et 2 A sur les deux autres,
|
||||
// et « un tiers du total » supposerait un équilibre que rien ne garantit. Ce qui se compare,
|
||||
// ce sont les MARGES — des watts dans les deux cas.
|
||||
|
||||
// ---- Aucun plafond : la marge est SANS OBJET, pas nulle -------------------------------
|
||||
// La distinction n'est pas cosmétique : une marge nulle bornerait tout à 0 W et couperait
|
||||
// l'installation. C'est le « zéro forgé » du contrat, appliqué à une couche de sécurité.
|
||||
const DrawMargin rien = resolveDrawMargin({}, {0, 0, 0}, 0);
|
||||
QVERIFY2(!rien.borne, "aucun plafond déclaré doit rendre une marge SANS OBJET");
|
||||
|
||||
// ---- Une seule source, par phase : c'est la phase la PLUS CHARGÉE qui décide -----------
|
||||
const DrawCap disjoncteur{DrawCapSource::Breaker, 5000, -1};
|
||||
DrawMargin m = resolveDrawMargin({disjoncteur}, {1000, 4500, 200}, 5700);
|
||||
QVERIFY(m.borne);
|
||||
QCOMPARE(qRound(m.watts), 500); // 5000 − 4500, la phase B
|
||||
QCOMPARE(m.source, DrawCapSource::Breaker);
|
||||
QCOMPARE(m.phase, 1); // et on DIT laquelle
|
||||
|
||||
// ---- Deux formes en concurrence : la plus petite marge mord ---------------------------
|
||||
// 3000 W restants au total contre 500 W sur la phase B : c'est la phase qui borne, alors
|
||||
// même que son plafond (5000) est plus BAS que le total (8700). Comparer les plafonds
|
||||
// aurait donné la réponse inverse.
|
||||
const DrawCap gestionnaire{DrawCapSource::GridOperator, -1, 8700};
|
||||
m = resolveDrawMargin({gestionnaire, disjoncteur}, {1000, 4500, 200}, 5700);
|
||||
QCOMPARE(qRound(m.watts), 500);
|
||||
QCOMPARE(m.source, DrawCapSource::Breaker);
|
||||
|
||||
// …et l'inverse, sur la même mesure : un ordre du gestionnaire plus serré prend la main, et
|
||||
// le motif publié doit alors dire « gestionnaire de réseau », jamais « surcharge ».
|
||||
const DrawCap ordre{DrawCapSource::GridOperator, -1, 6000};
|
||||
m = resolveDrawMargin({disjoncteur, ordre}, {1000, 4500, 200}, 5700);
|
||||
QCOMPARE(qRound(m.watts), 300); // 6000 − 5700
|
||||
QCOMPARE(m.source, DrawCapSource::GridOperator);
|
||||
QCOMPARE(m.phase, -1); // un plafond total n'a pas de phase
|
||||
|
||||
// ---- Marge NÉGATIVE : le plafond est déjà dépassé, et de combien ----------------------
|
||||
// C'est ce que la couche L4 doit délester. Écrêter à 0 perdrait l'ampleur du dépassement.
|
||||
m = resolveDrawMargin({disjoncteur}, {1000, 6200, 200}, 7400);
|
||||
QCOMPARE(qRound(m.watts), -1200);
|
||||
QCOMPARE(m.phase, 1);
|
||||
|
||||
// ---- Marges ÉGALES : le départage est STABLE, pas dépendant de l'ordre du tableau -----
|
||||
// Sans règle, le motif publié changerait au gré de l'ordre d'insertion — donc sans qu'aucune
|
||||
// décision n'ait changé, ce qui est exactement ce qu'un motif ne doit jamais faire.
|
||||
const DrawCap eco{DrawCapSource::EcoSelfImposed, -1, 6000};
|
||||
const DrawCap ordre2{DrawCapSource::GridOperator, -1, 6000};
|
||||
const DrawMargin a = resolveDrawMargin({eco, ordre2}, {2000, 2000, 2000}, 5700);
|
||||
const DrawMargin b = resolveDrawMargin({ordre2, eco}, {2000, 2000, 2000}, 5700);
|
||||
QCOMPARE(a.source, b.source);
|
||||
QCOMPARE(a.source, DrawCapSource::GridOperator); // imposé > auto-imposé
|
||||
|
||||
// ---- Les codes franchissent la frontière RPC, et ils sont fermés ----------------------
|
||||
QCOMPARE(drawCapSourceCode(DrawCapSource::Breaker), QStringLiteral("connection"));
|
||||
QCOMPARE(drawCapSourceCode(DrawCapSource::GridOperator), QStringLiteral("gridOperator"));
|
||||
QCOMPARE(drawCapSourceCode(DrawCapSource::EcoSelfImposed), QStringLiteral("selfImposed"));
|
||||
}
|
||||
|
||||
@ -75,6 +75,11 @@ private slots:
|
||||
//! La marque de rang s'efface sur un RANG changé, pas sur une écriture reçue — et un
|
||||
//! client ne peut pas fabriquer un "auto" (LM-1209-c).
|
||||
void testRankOriginFollowsTheRankNotTheWrite();
|
||||
|
||||
// ── LM-1006-1 — le plafond de soutirage, étape 1 ───────────────────────
|
||||
//! On compare des MARGES, jamais des plafonds : le plus contraignant mord, et on sait
|
||||
//! lequel. Aucun plafond déclaré ≠ marge nulle. Départage stable à marge égale.
|
||||
void testDrawMarginComparesHeadroomsNotCaps();
|
||||
};
|
||||
|
||||
#endif // TESTLOADMODEL_H
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user