Le trou nommé hier : la surface RPC n'est couverte par aucun test. Une méthode qui ne répond plus, en silence, pendant que 31 unitaires, la simulation et ci-quality passent. Simulation::testEveryGetterReturnsWhatItDeclares appelle les 14 méthodes NymeaEnergy.Get* et vérifie, dans cet ordre : le STATUT est success, la charge utile n'est pas vide, et chaque clé requise de `returns` est servie. Les Set* sont exclus — ils mutent, et les appeler à l'aveugle serait pire que le trou. MA PREMIÈRE VERSION ÉTAIT UNE CONTRE-ÉPREUVE INERTE. Elle disait « pour chaque clé déclarée, vérifier qu'elle est servie » : en retirant la déclaration de GetPvForecast, elle PASSAIT — il n'y avait plus rien à vérifier. Le défaut désactivait son propre détecteur, soit exactement le mode d'érosion catalogué dans AGENTS.md, rencontré dans le contrôle écrit POUR fermer un trou. Le critère est donc passé de la DÉCLARATION à la RÉPONSE : un getter qui ne rend rien est cassé, quelle que soit sa déclaration. C'est l'affirmation qui tient debout sans elle. ET LA CONTRE-ÉPREUVE A CORRIGÉ MON DIAGNOSTIC. J'avais écrit que nymea élimine la charge utile quand `returns` n'est pas déclaré. C'est FAUX, vérifié : la méthode répond correctement sans returns. Ce qui casse, c'est la déclaration des PARAMÈTRES — sans elle l'appel est rejeté à la validation (« Invalid params: Invalid key: from ») et le handler n'est jamais atteint. La différence n'est pas académique : un client qui ne lit que `params` voit une réponse vide et accuse le handler, qui n'a rien fait. J'avais le bon coupable et le mauvais mécanisme — conclusion vraie sur un raisonnement faux, et rien ne l'aurait montré sans une contre-épreuve qui rejoue le défaut. Le contrôle regarde donc le STATUT en premier, pour nommer le vrai coupable. Ce que le balayage NE couvre pas, écrit plutôt que sous-entendu : les Set* (non appelés), la FORME des valeurs (une clé présente ne dit rien de son contenu), et les paramètres OPTIONNELS (o:knownAt n'est jamais exercé). simulation 70/70, 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
246 lines
14 KiB
C++
246 lines
14 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea-energy-plugin-nymea.
|
|
*
|
|
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include <QtTest>
|
|
#include <QDateTime>
|
|
|
|
#include "simulationbase.h"
|
|
|
|
using namespace nymeaserver;
|
|
|
|
class ChargerPlugEvent
|
|
{
|
|
public:
|
|
ChargerPlugEvent() = default;
|
|
ChargerPlugEvent(const QDateTime &dt, bool pi = false, int pu = 0)
|
|
: dateTime{dt},
|
|
pluggedIn{pi},
|
|
percentageUsed{pu}
|
|
{}
|
|
QDateTime dateTime;
|
|
bool pluggedIn;
|
|
int percentageUsed = 0;
|
|
};
|
|
|
|
class Simulation: public SimulationBase
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
explicit Simulation(QObject *parent = nullptr) : SimulationBase(parent) { };
|
|
|
|
typedef QList<ChargerPlugEvent> ChargerPlugEvents;
|
|
typedef QList<int> DetailsStepList;
|
|
|
|
private slots:
|
|
void run_data();
|
|
void run();
|
|
|
|
// [T3] etmvariableload (ECS/routeur) : arrondi fixed (powerLevels) + clamp dynamic
|
|
// (maxPowerW), recrédit currentPowerW (anti-clignotement), délestage, round-trip powerSetpoint.
|
|
void testEcsSurplusPV();
|
|
|
|
// [T4] Watchdog L2 : compteur muet >90 s → etmvariableload setPowerSetpoint(0) force=true,
|
|
// planif suspendue (reste 0 sur N cycles), reprise au retour compteur.
|
|
void testMeterSilentFallback();
|
|
|
|
// [T4] Persistance LoadConfig (§4) : SetConfigs → fichier → relecture round-trip + validation.
|
|
void testLoadConfigPersistence();
|
|
// [T4] Construction des adaptateurs depuis la config : enabled servi, disabled exclu (§9).
|
|
void testLoadConfigBuildsAdapters();
|
|
// [T4] Injection RPC end-to-end : NymeaEnergy.Get/SetLoadConfig (couche consommée par l'app).
|
|
void testLoadConfigRpc();
|
|
// [rév.3] Chaîne complète : SetConfigs(relays[]) → rebuild → RelayRouter → cycle → commutation relais.
|
|
void testLoadConfigRelayRouter();
|
|
|
|
// SG-Ready (PAC) : montée d'états sur surplus, hystérésis 3↔4, protection court-cycling,
|
|
// + Volet 4 budget PARTAGÉ etmvariableload(ECS)↔PAC (waterfall unifié, inversion priorité).
|
|
void testSgReadySurplus();
|
|
|
|
// [rév.3] Combinatoire watts→relais DANS le RelayRouter : paliers dérivés, off-before-on,
|
|
// transition non-cascadée, et DÉDUPLICATION des niveaux (relais identiques fusionnés).
|
|
void testEcsRelayTopologies();
|
|
|
|
// [Phase 2] Ratios canoniques : EnergyRatiosCalculator aligné 1:1 sur le seam interim app
|
|
// (EnergyRatiosInterim.compute) — seed, normal, clamp, den≤0→n/a, non-monotone, nouveau jour.
|
|
void testEnergyRatiosAlignment();
|
|
|
|
// [étape 1 / ECS-306] Le budget décrémenté par le scheduler correspond au palier
|
|
// RÉELLEMENT applicable sous verrou minOn — les charges suivantes ne reçoivent pas un
|
|
// résidu surestimé.
|
|
void testEcsBudgetUnderLock();
|
|
// [étape 1 / ECS-411] Le palier courant est déduit de l'état réel des Things relais au
|
|
// démarrage, pas réinitialisé à 0.
|
|
void testEcsRestartRecovery();
|
|
// [étape 1 / ECS-412] SetLoadConfig pendant une fenêtre de verrou active ne réarme pas
|
|
// le verrou : seul le matériel modifié est reconstruit. Couvre aussi l'armement à froid.
|
|
void testEcsRebuildPreservesLock();
|
|
// [étape 1 / ECS-412] Démarrage à froid, palier 0, minOffS > 0, budget largement
|
|
// suffisant : la charge reste éteinte PENDANT minOffS puis s'enclenche. L'armement à
|
|
// froid doit être transitoire, jamais permanent (défaut constaté au banc 2026-08-09).
|
|
void testEcsColdStartLockExpires();
|
|
// [ECS-410] Échec d'écriture en cours de transition : retour arrière, arrêt total, puis
|
|
// état de défaut collant. Chaque marche de l'échelle a son cas, et la levée est délibérée.
|
|
void testEcsPartialFailure();
|
|
// [ECS-413] Désactiver une charge la laisse dans l'état SÛR de son adaptateur, pas dans
|
|
// son dernier état commandé — et un rebuild SANS désactivation ne coupe rien (cas négatif
|
|
// obligatoire, sans lui ECS-412 serait annulé).
|
|
void testEcsDisableLeavesSafeState();
|
|
// [ECS-414] L'échelle d'échec généralisée à SgReadyAdapter et EtmVariableLoadAdapter.
|
|
// Plancher propre à chaque adaptateur : ÉTAT 2 pour la PAC, jamais « contacts ouverts ».
|
|
void testSgReadyPartialFailure();
|
|
// [LM-300] Union discriminée : le mécanisme sg-ready se configure comme les autres, ses
|
|
// charges utiles sont mutuellement exclusives, et l'état 2 est obligatoire (ECS-110).
|
|
void testSgReadyFromConfig();
|
|
// [ECS-110 étendu] Un Thing n'appartient qu'à UNE charge active. Couvre le conflit
|
|
// ENTRE charges (relay-router ↔ sg-ready ↔ etmvariableload), les doublons intra-charge,
|
|
// les identifiants en double, et la tolérance d'une charge désactivée.
|
|
void testThingOwnershipIsExclusive();
|
|
|
|
// [Lot B §1] Rendu français des motifs : chaque code du catalogue produit MOT POUR MOT la
|
|
// phrase d'avant la refonte {code, params}. Le journal se prouve par comparaison, pas par
|
|
// relecture — et une phrase reformulée au passage ferait échouer ce test, pas un humain.
|
|
void testDecisionReasonRendering();
|
|
|
|
// [Lot B §2] Frontière de télémétrie : GetLoadTelemetry ≡ LoadTelemetryChanged, verrous en
|
|
// secondes, union discriminée de mécanisme, reprise à froid du mode dégradé, battement de
|
|
// cœur, et ZÉRO langue naturelle dans la charge utile.
|
|
void testLoadTelemetryRpc();
|
|
|
|
// [Lot B §3] o:domain : aller-retour lire→réécrire sans perte, valeur hors énumération
|
|
// rejetée, charge sans domaine acceptée — et aucun effet sur l'arbitrage.
|
|
void testLoadConfigDomain();
|
|
|
|
// [Lot B §2] La pure virtuelle force à RÉPONDRE, pas à répondre quelque chose : une vue
|
|
// vide compilerait tout aussi bien et sortirait de la télémétrie sans bruit. Ce test exige
|
|
// une vue SUBSTANTIELLE — mécanisme nommé, charge utile portant les valeurs runtime
|
|
// réelles, verrou en secondes qui expire — pour les QUATRE adaptateurs.
|
|
void testRuntimeViewIsSubstantial();
|
|
|
|
// [lot B-bis / ECS-411 + ECS-411-b appliqués au SG-Ready] L'état de départ est RELU sur les
|
|
// contacts, pas supposé — et le relevé publie ce qui a été LU, y compris quand l'issue est
|
|
// l'état neutre. Couvre aussi le corollaire LM-104 : une remise au neutre doit ÉCRIRE, sans
|
|
// quoi l'hypothèse de prudence devient un masquage de panne.
|
|
void testSgReadyRestartRecovery();
|
|
|
|
// ── 3g-2 ───────────────────────────────────────────────────────────────
|
|
// [LM-1206/LM-1207] Le rang d'une borne vit dans LoadConfig, comme celui de toute autre
|
|
// charge — et l'inverser change laquelle démarre. C'est la moitié ARBITRAGE de LM-1204-b :
|
|
// le banc en garde la moitié matérielle, qu'aucune wallbox simulée ne peut prouver.
|
|
// Couvre aussi « une charge, un commandeur » : la borne refusée est éteinte par l'arbitre,
|
|
// et le proxy ne repasse pas derrière avec une consigne de son cru.
|
|
void testEvChargerRankDecidesWhoCharges();
|
|
|
|
// [3g-2] Le démarrage sous tolérance d'acquisition est une décision d'ACHETER : elle porte
|
|
// un financement réseau, un motif qui chiffre le soutirage, et une comptabilité partagée.
|
|
void testEvGridStartIsAnnouncedAndBounded();
|
|
|
|
// [banc 2026-08-27] Une borne SANS VÉHICULE BRANCHÉ ne peut rien consommer de ce qu'on lui
|
|
// allouerait : elle doit être HORS arbitrage, comme une borne sans voiture ou en mode
|
|
// manuel. Sans ce filtre elle absorbe le budget de la cascade et le donne à personne —
|
|
// 4 853 W mesurés au banc, la charge suivante n'ayant survécu que sur son recrédit.
|
|
void testUnpluggedChargerTakesNoBudget();
|
|
|
|
// [3g-2] L'entrée de configuration d'une borne SURVIT à la disparition de son Thing : elle
|
|
// se lit comme une charge dont l'appareil manque, pas comme une charge jamais configurée.
|
|
void testEvChargerConfigOutlivesItsThing();
|
|
|
|
// [LM-1208-b, banc 2026-08-28] Une borne SANS VOITURE ASSIGNÉE est arbitrée — le cas de
|
|
// l'invité. Et elle l'est avec le nombre de phases de la BORNE : le manager ne la prépare
|
|
// pas, donc son ChargingProcessInfo n'existe pas, et le lire par défaut annoncerait
|
|
// monophasée une borne triphasée en divisant le budget par trois, silencieusement.
|
|
// Le mode manuel, lui, reste une exclusion : la seconde moitié du test le prouve.
|
|
void testChargerWithoutCarIsArbitrated();
|
|
|
|
// ── §11 / LM-1105 + LM-1106 ────────────────────────────────────────────
|
|
// La mesure d'une charge se lit sur l'APPAREIL PILOTÉ, sans qu'on ait à le désigner comme
|
|
// son propre compteur — et ce qui est publié est un COUPLE {valeur, source}. Les trois
|
|
// sources sont éprouvées, plus les deux règles de priorité : le compteur explicite gagne,
|
|
// mais un compteur injoignable fait retomber sur l'appareil au lieu de taire la mesure.
|
|
void testMeasurementSourceIsPublished();
|
|
|
|
// [R3] Σ counts == targetW sur TOUTES les charges publiées, à chaque cycle, quel que soit
|
|
// le chemin qui a construit l'action. C'est une relecture de la charge utile, pas une
|
|
// confiance faite à sept sites de construction : un site oublié se voit ici, jamais en
|
|
// relisant le code. Couvre aussi le partage d'EV_GRID_START et la présence de counts à
|
|
// zéro avec sa destination.
|
|
void testCountsSumToTarget();
|
|
|
|
// [R1 + R7] L'ABSENCE et le ZÉRO disent deux choses différentes : un niveau à targetW 0 est
|
|
// publié (« passe parcourue, rien à prendre »), un niveau absent dirait « passe non
|
|
// parcourue ». Et decision.level accompagne levels[] : les deux présents, ou les deux
|
|
// absents — en mode dégradé il n'y a pas de passe, donc ni l'un ni l'autre.
|
|
void testLevelAbsenceAndZeroDifferInMeaning();
|
|
void testPhaseAllowanceReadsTheStateItTests();
|
|
void testDrawCapBoundsTheCascade();
|
|
void testDrawAuthorisationSurvivesTheBatteryReserve();
|
|
void testEcoProgressBaseAndRegime();
|
|
void testEcoPassBuysAndSaysTheTrajectory();
|
|
void testEcoFloorMissedSaysWhatWasLostAndWhy();
|
|
void testCommandDivergenceIsPublishedNotDiagnosed();
|
|
void testProgressMeasurabilityIsDeclaredAtConfigTime();
|
|
void testThermalProbeFeedsTelemetry();
|
|
void testHealthyButUnmeasuredDiffersFromFaulty();
|
|
void testLifetimeCounterDropNeverRebasesTheProgress();
|
|
void testWorstPhaseBindsWhenPhasesAreUnbalanced();
|
|
void testPhaseVoltageDoesNotLeakBetweenPhases();
|
|
void testMeterEnergyActuallyAccumulates();
|
|
void testRecreditIsALoanAgainstObedience();
|
|
void testARefusedStateIsDistinguishableFromAnApplied();
|
|
void testL4ShedsAPacAndSaysItForcedTheLock();
|
|
void testDerogationTakesTheHeadButNotTheSafety();
|
|
//! TOUTE méthode Get* du namespace rend ce que sa déclaration promet.
|
|
void testEveryGetterReturnsWhatItDeclares();
|
|
void testAModeWriteEndsTheDerogationAndKeepsTheChoice();
|
|
|
|
// ── §12 / LM-1209 ──────────────────────────────────────────────────────
|
|
// Une borne détectée après la mise en service entre EN QUEUE : elle ne passe devant
|
|
// aucune charge qu'un humain a classée, aucun autre rang ne bouge, le rang produit est
|
|
// libre par construction, et l'entrée porte rankOrigin="auto" — « ce rang est un défaut ».
|
|
void testAutoProvisionedRankGoesToTheQueue();
|
|
|
|
// [LM-1209-e] Supprimer l'entrée d'une borne dont le Thing VIT la fait RECRÉER au cycle
|
|
// suivant — en queue, et de nouveau en rankOrigin "auto" : personne ne l'a classée. Sans
|
|
// ce rattrapage, la borne reste arbitrée mais absente de GetLoadConfig, à un rang non
|
|
// réglable — le trou de 3g-2 rouvert par le côté client.
|
|
void testDeletedEvChargerConfigComesBack();
|
|
|
|
// [ECS-111] Un relais DÉCLARÉ qui disparaît du registre nymea : la charge sort de
|
|
// l'arbitrage (available faux, THING_MISSING) et reste dans la comptabilité, figée à la
|
|
// puissance crue engagée. Avant, elle publiait le nominal du palier entier — part du
|
|
// relais fantôme comprise — et ce chiffre entrait dans le RECRÉDIT, donc dans le budget.
|
|
void testVanishedRelayFreezesTheLoad();
|
|
|
|
void printStates(Thing *thing);
|
|
void updateChargerMeter(Thing *thing);
|
|
|
|
QStringList plotOriginalData(int powerBalanceCount);
|
|
QStringList plotSimulation(const QString &title, int powerBalanceCount);
|
|
QStringList plotSpotMarketData(int powerBalanceCount);
|
|
|
|
};
|
|
|
|
Q_DECLARE_METATYPE(ChargerPlugEvent)
|
|
Q_DECLARE_METATYPE(Simulation::ChargerPlugEvents)
|
|
Q_DECLARE_METATYPE(Simulation::DetailsStepList)
|