Le canal est livré (openmeteo 1.15.2+etm11) : forecastCurveTransport, une QVariantMap TYPÉE déclarée dans la ThingClass — au contrat, sans seconde couche d'encodage. Mieux que la chaîne JSON envisagée : le §12.5 réclamait une version dans la charge utile faute de schéma ; ici la charge utile a un schéma ET porte sa version. issuedAt VOYAGE DANS LA CHARGE UTILE. La clé de révision devient exacte, et tout le §13 est levé : receivedAt, l'interdiction d'en calculer un horizon, la dette meta.json. targetAt - issuedAt EST l'horizon. Le coût que je chiffrais disparaît avec sa cause. CE QUE J'AVAIS FAUX. Ma déduplication REJETAIT une révision aux valeurs identiques. Avec une vraie heure de run, c'est faux : un run plus récent portant les mêmes chiffres CONFIRME, il ne répète pas. Le rejeter rendrait indistincts « le modèle a confirmé quatre fois » et « le modèle n'a rien dit depuis ce matin ». Le mécanisme change donc de nature et se simplifie : la CLÉ déduplique par construction, et la comparaison de contenu ne fait plus que JOURNALISER (« le run confirme le précédent »). Si issuedAt bougeait un jour sans que la prévision bouge, cela se verrait dans le nombre de révisions — un défaut à la source, qu'il faut voir et non masquer. Une déduplication de contenu l'aurait effacé. La comparaison sur l'intersection reste, mais comme précaution : la fenêtre couvre désormais les journées entières et les révisions sont alignées. Elle ne dépend d'aucun comportement du voisin dont personne ne nous préviendrait. model porte la provenance réelle, repli best_match compris. La colonne cesse d'être nulle ; la règle de non-remplissage rétroactif reste. L'INDEXATION EST LUE, PAS SUPPOSÉE. `convention` dit que p[i] est la moyenne sur [t0 + i·step, t0 + (i+1)·step) : l'énergie vaut p[i] x step / 3600, sans supposer que le pas vaut une heure. `unit` est VÉRIFIÉE — lire des Wh comme des W donnerait une courbe plausible, fausse d'un facteur qui dépend du pas. Lecture branchée dans le cycle, et c'est sans coût : un run déjà connu n'écrit rien. Attendre une notification serait plus élégant et MOINS SÛR — un état publié avant notre démarrage ne notifierait jamais, et la courbe resterait invisible jusqu'au run suivant. L'état est cherché par son NOM, jamais par un identifiant de ThingClass. Un refus est journalisé une fois par MOTIF, pas une fois par cycle. Deux tests : l'identité du run est celle de la révision (avec l'assertion centrale — un run qui confirme EST conservé), et le refus de charge utile plutôt que l'interprétation au mieux (version, unité, vide, tronquée, et un créneau illisible qui reste un TROU). loadmodel 30/30, charging 48/48, spotmarket 32/32, simulation 69/69, ci-quality 4/4, build 0 erreur 0 warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
100 lines
4.7 KiB
C++
100 lines
4.7 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();
|
|
|
|
//! L'identité du RUN est celle de la révision — et un run qui confirme n'est pas un doublon.
|
|
void testTheRunIdentityIsTheRevisionIdentity();
|
|
//! La courbe se relit telle qu'elle était connue à un instant, et ses trous restent des trous.
|
|
void testTheCurveIsReproducibleAndKeepsItsHoles();
|
|
//! La rétention garde les DEUX bouts : n'en garder qu'un détruirait la révision.
|
|
void testRetentionKeepsBothEndsOfEachRevision();
|
|
//! La charge utile de transport se REFUSE plutôt que de s'interpréter au mieux.
|
|
void testAnUnknownPayloadIsRefusedNotBestEffort();
|
|
};
|
|
|
|
#endif // TESTLOADMODEL_H
|