Patrick Schurig 6b66d49f27 feat(prévision PV): lire forecastCurveTransport v1 ET v2, plafond AC de site (brief 3, T1)
Prérequis du déploiement d'openmeteo v2 (§28) :
- `depuisTransport()` lit v1 et v2. v2 exige `corrections` (liste) ; une
  correction inconnue refuse le plan (`CORRECTION_<nom>`), `ac-limit` compris,
  qui appartient au site. Autre version : `VERSION_<v>`, jamais devinée.
  `gti[]`/`tempC[]` ne sont pas lus ici (enregistrement : T2).
- `acLimitW`, paramètre de SITE porté par `Get/SetForecastSite` : optionnel,
  absent = aucun plafond (jamais 0), borné [100, 1 000 000] W, refusé sans
  site ; la désignation se remplace en entier. Persisté (`forecastAcLimitW`).
- `ForecastStore::sommerPlans()` : somme des plans du même run ET du même pas,
  puis écrêtage à `acLimitW × pas / 3600` APRÈS la somme. Fonction unique pour
  la table et le snapshot.
- `GetForecastSnapshot` publie `corrections` (intersection si les plans
  diffèrent, journalisé au changement ; `ac-limit` si plafond configuré),
  `conversion: "physics"` et `acLimitW`.

Aucune décision ne lit la prévision (§11) ; boucle de sécurité ni appelée ni
modifiée ; pas de `*Changed`.

Tests : loadmodel `testTransportV2IsReadAndAnUnknownCorrectionRefused`,
`testTheSiteCurveIsClippedAfterTheSum` (contre-épreuve faite : plafond
neutralisé → échec) ; simulation `testTheSiteAcLimitTravelsWithTheDesignation`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:59:27 +02:00

105 lines
4.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 testEcoUnmeasurableBuysAtAConstantRate();
void testMinimumIsServedEvenWhenTheObligationIsMet();
void testStoredInstantsSurviveATimeZoneChange();
void testReportingDayBoundaryIsPublishedNotDeducible();
void testDrawMarginComparesHeadroomsNotCaps();
//! Une révision n'existe que si la PRÉVISION a changé — et le contenu seul en décide.
void testTheContentIsTheRevisionIdentity();
//! La courbe se relit telle qu'elle était connue, 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 courbe COURANTE est la dernière VUE, pas la dernière apparue.
void testTheCurrentCurveIsTheLastSeenNotTheLastNew();
//! La charge utile se REFUSE plutôt que de s'interpréter au mieux — mais pas pour un attribut.
void testAPayloadIsRefusedOnFormNeverOnAMissingAttribute();
void testTransportV2IsReadAndAnUnknownCorrectionRefused();
void testTheSiteCurveIsClippedAfterTheSum();
};
#endif // TESTLOADMODEL_H