Ce qui ne dépend pas du canal, écrit maintenant. La lecture se branchera quand la forme du canal openmeteo sera connue ; le magasin est vide d'ici là, et une courbe vide EST la réponse honnête à « que prévoit-on ? » quand personne n'a rien publié. LA CLÉ EST LA PAIRE (targetAt, receivedAt). Avec targetAt seul, chaque rafraîchissement écraserait le précédent : on perdrait les révisions, donc la comparaison prévu/réalisé — ce que ce lot existe pour montrer. Le GPL n'apprend pas, il rend l'apprentissage possible : cette table est la matière, et l'écraser la détruirait pour toujours sans que rien ne le signale. receivedAt, JAMAIS issuedAt, et l'interdiction est écrite et non seulement nommée : aucun horizon ne se calcule depuis ce champ. Entre l'initialisation d'un run et sa mise à disposition s'écoule la latence de publication du modèle — des heures. targetAt - receivedAt est donc trop court, systématiquement et dans le même sens. « Quelle prévision » est juste, « quel horizon » est faux. LA DÉDUPLICATION PORTE SUR L'INTERSECTION, pas sur la charge utile. Si la fenêtre glisse, la courbe de 11 h a un point de moins que celle de 10 h : une égalité de charge utile verrait une révision toutes les heures, exactement ce que la déduplication existe pour éviter. Et `communs > 0` est requis — sans heure commune il n'y a rien à comparer, et conclure « identique » ferait disparaître une révision dont on ne sait rien (cas du passage de minuit). Ce choix ne dépend pas de la forme que le producteur publie aujourd'hui : il est correct que la fenêtre glisse ou non, donc il ne peut pas être invalidé par un changement chez le voisin dont personne ne nous préviendrait. RÉTENTION : sept jours pleins, puis les DEUX BOUTS par heure visée — la première révision (horizon le plus long) et la dernière (meilleure estimation). Les deux, et pas une : elles répondent aux deux seules questions qui se posent, et n'en garder qu'une détruirait la révision. model reste NULL tant que le producteur publie une INTENTION et non une provenance. Une chaîne vide serait déjà une valeur ; NULL dit « on ne sait pas », ce qui est vrai. Aucun remplissage rétroactif à prévoir. GetPvForecast : un Get* SANS *Changed — amendement assumé à l'invariant 11, qui vise l'ÉTAT DE L'ARBITRE, celui qui bouge à chaque cycle et laisse un client déconnecté en retard. Une courbe historisée se lit sur demande, comme GetPowerBalanceLogs ; notifier chaque révision produirait un message par heure sur une donnée que personne n'attend en direct. `knownAt` rend la lecture reproductible : « ce qu'on prévoyait ce matin » se demande en le fixant. Base SÉPARÉE, jamais energylogs.sqlite : celle-là appartient à l'amont qui en migre le schéma, et surtout la clé y est un instant de mesure quand la nôtre est une paire. Une prévision révisée n'a rien à faire dans la base du mesuré. Trois tests unitaires, et le premier vise le cas qu'un == naïf raterait : une fenêtre glissée d'une heure, valeurs communes inchangées, charge utile différente — ce n'est PAS une révision. Avec ses contre-épreuves : une valeur commune modifiée EN EST une, et une fenêtre sans heure commune aussi. loadmodel 29/29, 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
99 lines
4.6 KiB
C++
99 lines
4.6 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();
|
|
|
|
//! Une révision identique SUR SES HEURES COMMUNES n'en est pas une — et c'est le cas qu'une
|
|
//! égalité de charge utile raterait dès que la fenêtre glisse.
|
|
void testARevisionIsJudgedOnTheOverlapNotThePayload();
|
|
//! 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();
|
|
};
|
|
|
|
#endif // TESTLOADMODEL_H
|