Patrick Schurig e65dbb4a6a feat(§10): « Surplus + minimum réseau » — le mode sert enfin un minimum
LM-1013-b-vi. Le mode ChargingModeEcoWithMinCurrent promettait un courant minimum
depuis toujours sans qu'aucun chemin ne le serve : mort par réécriture chez
l'amont, puis mort par exclusion quand le waterfall a pris la borne.

LA LOI. computeEcoTarget() rend max(cadence de l'obligation, minimum). Le corps
historique est isolé dans calculerObligation() et une sortie anticipée rend
EXACTEMENT l'ancien résultat quand aucun minimum n'est demandé — ce n'est pas une
optimisation, c'est la garantie que l'ajout ne déplace rien pour les charges qui
n'en veulent pas. Les quatre suites le confirment.

LE MINIMUM NE SE TIENT JAMAIS. `tenue` ne décrit que l'obligation en énergie ;
quand elle est tenue, la cadence vaut 0 et la consigne retombe SUR LE MINIMUM, pas
sur zéro. C'est là, et seulement là, que la différence de nature entre une énergie
avec échéance et une puissance permanente se voit dans le code. L'ordonnanceur ne
publie donc ECO_FLOOR_MET que si watts vaut aussi 0 : sinon il couperait ce qu'il
doit continuer de servir.

DÉRIVÉ, JAMAIS STOCKÉ. EvAdapter calcule le minimum depuis evFloorW() à chaque
cycle quand le mode le demande. L'écrire en configuration à l'appairage créerait
une copie qui se périme au premier recâblage — phaseCount change, la copie reste
fausse en silence. Correction apportée par le brief app f2bd07d, et elle est
juste.

LE MOTIF DIT LEQUEL DES DEUX MORD. ECO_FLOOR_GRID gagne minAlwaysW et boundBy, et
OMET remainingWh/targetWh/remainingS/regime sur une charge à minimum seul : un
remainingS à 0 inventerait une échéance imminente et un remainingWh à 0 une
obligation tenue. progress est absent pour la même raison — il ne décrit que
l'obligation.

CONTRAT. Les trois descriptions de ChargingMode sont corrigées. Normal n'est pas
« recharge immédiate au maximum » : c'est MANUEL, le moteur ne commande rien, et
seule la protection de surcharge la touche encore — ce qui doit être dit, un mode
qui promet l'abstention et coupe quand même étant une surprise. Et les mots
« garanti » et « prioritaire » sont bannis à propos de ce mode : le premier promet
une certitude que rien ne tient, le second est vrai face au confort et faux face à
un autre besoin éco mieux rangé, donc faux exactement dans le cas d'échec.

TEST vu mordre : désarmé, minSeul.watts rend 0 au lieu de 4 140. Cinq points, dont
la contre-épreuve qui distingue « 4 140 vient du minimum » de « 4 140 vient d'un
reliquat d'obligation », et le cas 1 qui vérifie que rien ne bouge sans minimum.

Suites : loadmodel 26/26 (+1), simulation 67/67, charging 48/48, spotmarket 32/32.
NON DÉPLOYÉ — +etm57 attend le feu vert.
2026-09-07 11:20:08 +02:00

91 lines
4.1 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef TESTLOADMODEL_H
#define TESTLOADMODEL_H
#include <QObject>
/*!
* \brief Logique pure du modèle de charge : table de paliers et identité matérielle.
*
* Aucun serveur nymea ici. Ce qui est éprouvé ne dépend ni d'un ThingManager ni d'un
* cycle d'arbitrage : la dérivation des paliers est une combinatoire, et
* \c EnergyArbitrator::sameHardware() une comparaison.
*/
class TestLoadModel : public QObject
{
Q_OBJECT
private slots:
//! La table dérive de TOUTES les sommes de sous-ensembles, triées, 0 compris.
void testStageTableIsDerivedFromSubsetSums();
//! Puissances distinctes : l'ordre de déclaration n'a aucun effet observable.
void testRelayOrderIsInertWhenPowersAreDistinct();
//! Puissances égales : l'ordre décide QUEL contact sert le palier — et ça se voit.
void testRelayOrderDecidesWhichContactServesATiedStage();
//! Un réordonnancement sans effet ne doit PAS coûter une reconstruction.
void testReorderingDistinctRelaysKeepsTheAdapter();
//! Mais un échange de contacts à puissance égale, si.
void testSwappingTiedRelaysRebuilds();
//! Libellé, rang et besoins se mettent à jour en place : jamais de reconstruction.
void testSoftFieldsNeverRebuild();
// ── §11 — compteur et sonde par charge ─────────────────────────────────
//! Les deux champs traversent l'aller-retour, et sont OMIS quand ils sont vides.
void testMeasurementThingsRoundTrip();
//! Ils ne sont pas des canaux de commande : ils n'entrent pas dans claimedThingIds().
void testMeasurementThingsAreNotClaimed();
//! Donc deux charges peuvent partager le même compteur et la même sonde.
void testTwoLoadsMayShareAMeter();
//! Et les changer ne reconstruit rien : ils n'ont aucun effet sur le matériel.
void testMeasurementThingsAreSoftFields();
// ── §12 — le plancher de modulation ────────────────────────────────────
//! Déclaré en dynamic, et il traverse l'aller-retour.
void testFloorIsDeclaredForContinuousLoads();
//! Interdit là où il se DÉRIVE : deux sources d'une même vérité divergent.
void testFloorIsRefusedWhereItWouldBeDerived();
//! Un plancher au-dessus du plafond rendrait la charge inatteignable.
void testFloorAboveCeilingIsRefused();
// ── 3g-2 — le rang de la borne vit dans LoadConfig (LM-1206/LM-1207) ───
//! Une entrée evcharger est valide SANS charge utile, et traverse l'aller-retour.
void testEvChargerConfigIsRankOnly();
//! Toute charge utile d'un autre mécanisme y est refusée (LM-300, union discriminée).
void testEvChargerConfigRefusesPayloads();
//! Elle revendique son Thing : une borne ne peut pas être aussi une charge pilotée.
void testEvChargerClaimsItsThing();
// ── §12 / LM-1209 — l'auto-provisionnement remplit un trou, il ne décide pas ───
//! L'unicité de rang REFUSE une écriture, mais n'écarte JAMAIS au chargement : les deux
//! vérifications sont séparées parce que leurs gestes le sont (LM-1209-a).
void testRankUniquenessRefusesWriteButNeverDiscards();
//! 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 testEcoSpreadHardensAndSaysSo();
void testEcoUnmeasurableBuysAtAConstantRate();
void testMinimumIsServedEvenWhenTheObligationIsMet();
void testStoredInstantsSurviveATimeZoneChange();
void testReportingDayBoundaryIsPublishedNotDeducible();
void testDrawMarginComparesHeadroomsNotCaps();
};
#endif // TESTLOADMODEL_H