Patrick Schurig 8d035b0c1a feat(§10 étape D, 1/2): l'étalement uniforme, mesuré avant d'être asserté
`computeEcoTarget()` convertit une obligation en énergie en une cible en
watts : cadence = reste / temps restant, recalculée à chaque cycle. Une
cadence figée au départ ne rattrape pas — après une heure sans surplus elle
reste à la cadence initiale et manque l'échéance en ayant l'air de
travailler.

Mesurer d'abord était nécessaire, pas rituel : aucun scénario existant ne
portait d'obligation éco, donc aucune référence. Les valeurs ont été
relevées par sonde avant toute assertion — et c'est le relevé qui a trouvé
un défaut que la lecture du code ne montrait pas.

LA BASCULE DE PÉRIODE (LM-1013-c). À l'instant de l'échéance quotidienne,
resolveDeadline() vise la prochaine occurrence : une nouvelle obligation
s'ouvre. Sans reprise de la base d'avancement, le lendemain hérite de
l'énergie de la veille et se croit tenu sans avoir rien reçu. La sonde
affichait 83 W et 86400 s restantes à 07:00 pile, ce qui n'avait aucun sens
tant qu'on ne cherchait pas la période. Contre-épreuve : 1800 Wh portés au
crédit du jour suivant.

Limite assumée et écrite : l'obligation précédente, si elle n'était pas
tenue, est abandonnée SANS motif dédié. Le dire demande un ECO_FLOOR_MISSED
que rien n'émet ; le journal le dit, l'écran non.

Le durcissement est relevé point par point — 333 W à six heures, 2 kW à une
heure, le plafond dès seize minutes — parce que c'est la courbe que le motif
devra rendre lisible, pas une valeur isolée. Point de non-retour nommé
plutôt que subi : un seuil se discute, une asymptote non.

R6 armée dans le calcul : sous `unmeasurable`, jamais « tenue ». On vise
l'obligation entière, le choix qui sert plutôt que celui qui suppose acquis.

Suite : loadmodel 22/22, simulation 56/56, charging 48/48 identique à la
référence ligne à ligne, spotmarket 32/32, doxygen 0 avertissement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-31 12:40:07 +02:00

87 lines
3.9 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 testDrawMarginComparesHeadroomsNotCaps();
};
#endif // TESTLOADMODEL_H