Patrick Schurig 0a88a0f97f commentaires : la marge de progression renvoie au document de conception
La justification de ProgressionMinW cite la négociation la plus lente
par son document (DESIGN_COMMANDE_PERDUE.md §9.1) plutôt que comme un
relevé. Dans les tests, « banc » désignait le scénario : mot remplacé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:58:17 +01:00

400 lines
24 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"
#include <functional>
#include <memory>
class EnergyArbitrator;
struct EcritureBatterie;
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:
// ── Lot e1 — scénario commun ──────────────────────────────────────────────────────────
//! Quelle batterie pose le scénario.
enum class BatterieHc { Aucune, Pilotable, SansEtats, SansMinuteur };
//! Le banc d'un scénario d'heures creuses, et le journal des écritures sur la batterie.
struct ScenarioHc {
EnergyArbitrator *arb = nullptr;
Thing *meter = nullptr;
Thing *borne = nullptr;
Thing *batterie = nullptr;
QUuid borneId;
std::shared_ptr<QList<struct EcritureBatterie>> ecrites; //!< Lues à la frontière nymea.
std::shared_ptr<QDateTime> cycleCourant; //!< Date les écritures.
};
//! Quelle borne pose le scénario (lot e2) : 16 A, monophasée, triphasée, ou triphasée à
//! bascule de phases. La voiture suit : monophasée avec la première, triphasée sinon.
enum class BorneHc { Monophasee, Triphasee, Bascule };
//! Serveur neuf, compteur, borne 16 A « Éco avec minimum » (voiture débranchée), batterie.
ScenarioHc preparerScenarioHc(const QString &nom, BatterieHc batterie, const QVariantList &tarif,
BorneHc borne = BorneHc::Monophasee);
//! Un cycle à l'instant \p a, compteur à \p compteurW (import > 0) ; rend la trame publiée.
QVariantMap cycleHc(ScenarioHc &s, const QDateTime &a, double compteurW);
// ── Recrédit — une borne mesurée par un compteur dédié, cycle par cycle ──────────────────
//! Ce que la télémétrie dit de la borne à un cycle, et ce qu'elle a reçu.
struct CycleBorne {
int mesureW = 0; //!< ce que la borne tire au début du cycle
QString motif;
int refusDepuisS = -1;
int retenuW = 0;
int surplusW = 0;
int recrediteW = 0;
int consigneA = 0; //!< ce que la borne a reçu ; 0 = coupée
int consigneW = 0;
int attenduW = 0;
bool divergent = false;
};
//! Tirage de la borne au cycle \p k, depuis la commande reçue au cycle précédent et le
//! tirage précédent (W). C'est le modèle de la borne : rampe, refus, figeage.
using TirageBorne = std::function<double(int k, double commandeW, double tireW)>;
/*!
* \brief Rejoue \p cycles cycles de 60 s d'une borne monophasée 6-16 A, maison à 300 W.
* \param nom Suffixe du fichier de configuration. \param tirage Modèle de la borne.
* \param productionW Production au cycle k (W). \param cycles Nombre de cycles.
* \return La trace, cycle par cycle ; vide si le scénario n'a pas pu être posé.
*/
QList<CycleBorne> rejouerBorne(const QString &nom, const TirageBorne &tirage,
const std::function<double(int)> &productionW, int cycles);
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();
//! Correctif a — le motif publié est celui de la part majoritaire de la consigne fusionnée.
void testTheDecisionDescribesTheMergedAction();
//! Correctif c — l'acheté se compte sur l'énergie mesurée, à la part éco de l'appliqué.
void testPurchasedIsCountedOnTheMeasuredEnergy();
//! Sous le premier palier, le niveau éco dit « rien n'est acheté » — consigne inchangée.
void testEcoBelowTheFirstStageBuysNothing();
void testCommandDivergenceIsPublishedNotDiagnosed();
void testProgressMeasurabilityIsDeclaredAtConfigTime();
void testThermalProbeFeedsTelemetry();
void testHealthyButUnmeasuredDiffersFromFaulty();
void testLifetimeCounterDropNeverRebasesTheProgress();
void testWorstPhaseBindsWhenPhasesAreUnbalanced();
void testPhaseVoltageDoesNotLeakBetweenPhases();
void testMeterEnergyActuallyAccumulates();
void testRecreditIsALoanAgainstObedience();
//! Une marche de consigne suivie ne fait plus suspendre le recrédit ; la consigne tient.
void testRecreditSuspendedAfterAnObeyedStep();
//! Un vrai refus reste vu au cycle qui suit la commande ; la descente n'a pas d'excuse.
void testARealRefusalIsStillSeen();
//! Relais et consignes sous compteur dédié : marche suivie, refus réel, sans compteur.
void testWattLoadsFollowedOrRefusedUnderAMeter();
//! SG-Ready sous compteur dédié : la divergence se publie, le recrédit n'en dépend pas.
void testSgReadyDivergenceIsPublishedOnly();
//! P2 — un arrêt verrouillé engage le courant minimal : gelé au budget, aucun faux refus.
void testLockedStopFreezesTheMinimumAndIsNoRefusal();
//! P2 — le verrou d'arrêt borne la somme éco + confort, il ne s'y ajoute pas.
void testALockedStopDoesNotAddToTheEcoFloor();
void testARefusedStateIsDistinguishableFromAnApplied();
void testL4ShedsAPacAndSaysItForcedTheLock();
void testDerogationTakesTheHeadButNotTheSafety();
//! Brief 3 T0 — aucun objet réseau, aucune activation, un refus codé.
void testSpotMarketIsOutOfTheBuild();
//! Brief 3 T1 — acLimitW voyage avec la désignation de site, absent = aucun plafond.
void testTheSiteAcLimitTravelsWithTheDesignation();
void testTheSnapshotPublishesEachPlanesCorrections();
void testTheForecastSiteIdIsMatchedWhateverItsBraces();
void testAnUnreadableForecastDateIsAnError();
//! Brief 3 T2 — le cycle alimente l'enregistrement : paramètres du Thing, v1/v2, producteur.
void testTheLearningLogIsFedByTheCycle();
//! Brief 3 T4 — Get/SetTariff : aller-retour, refus codés sans effet, persistance, effacement.
void testTheTariffRpcRoundTripsAndRefuses();
//! Brief 3 T4 — la base retranche chaque charge selon SA source ; pas de compteur, pas de minute.
void testTheBaseloadSubtractsEachLoadBySource();
//! Brief 3 T4 — base et prix sur la grille de pv ; grille par défaut sans courbe ; 25 h.
void testTheSnapshotCarriesBaseloadAndPriceOnThePvGrid();
//! Brief 3 T4, §11 — un tarif et un profil n'ont AUCUN effet sur le plan.
void testNoDecisionReadsBaseloadOrPrice();
// ── Lot e1 — heures creuses pour la borne, batterie protégée pendant sa charge ────────
//! La journée de la démo : surplus à midi, attente des HC à 18 h, reprise à 22:00, décharge
//! bloquée tenue (minuteur réarmé) puis LEVÉE à la fin de charge ; jamais la charge réseau.
void testOffPeakDemoDay();
//! 06:00 : la borne s'arrête et la batterie est rendue — jour de 24, 23 et 25 heures.
void testOffPeakEndsAtSixAndOnDstDays();
//! Sans tarif, tarif unique, zones au même prix : le moteur est etm74 (minimum, ni bloc ni écriture).
void testWithoutOffPeakTheEngineIsEtm74();
//! Batterie sans états de commande, sans minuteur, absente : lue comme avant, motif publié.
void testABatteryWithoutControlIsReadAsBefore();
//! Le soutirage que le blocage ajoute se MESURE au cycle suivant ; L4 et le plafond décident.
void testBlockedDischargeShowsAtTheMeterAndL4Decides();
//! L2 : compteur muet, la batterie est levée explicitement, la trame du repli le dit.
void testAMeterSilenceHandsTheBatteryBack();
//! Onduleur qui REFUSE : poses espacées (1 → 15 min), un avertissement par série, `held` faux ;
//! une pose acceptée rend le rythme normal ; la levée n'attend jamais.
void testRefusedWritesAreSpacedAndNeverDelayTheRelease();
//! Lot e2 — en heures creuses, la borne reçoit son PLAFOND (3 680 W en mono, 11 040 W en tri) ;
//! hors heures creuses inchangé ; voiture pleine sous 230 W : la batterie est rendue.
void testOffPeakChargesAtFullPower();
//! Lot e2 — borne à bascule : en heures creuses, trois phases même quand le compteur lit 0.
void testOffPeakFullPowerSwitchesToThreePhases();
//! Lot e2 — limite de phase, compteur qui voit la borne : la borne est réduite, jamais sous son
//! plancher par la stratégie ; la marge d'une phase ne bride pas une triphasée (LM-1500-h) ; le
//! courant rendu est le MESURÉ ; en dépassement la consigne baisse ; L4 coupe au dépassement.
void testOffPeakFullPowerYieldsToThePhaseLimit();
//! Lot e2, suite — compteur qui voit la borne, maison à 2 A : la consigne se pose sans
//! alterner (mono et tri, limites 25 et 32 A), et sous une limite de 16 A elle se pose dessous,
//! à l'ampère inférieur.
void testOffPeakFullPowerSettlesUnderThePhaseLimit();
//! Lot e2, suite — compteur qui ne voit pas la borne : le retour de son courant ne la mène pas
//! seule à la limite (maison sans la borne écrêtée à zéro) ; total aveugle aussi : le surplus
//! fabriqué ne la porte plus au-delà du plafond (15 A, jamais 16).
void testOffPeakFullPowerStaysUnderTheLimitOnABlindMeter();
//! Lot e2, suite (écart 4) — à l'aube en heures creuses, le surplus prend la place de l'achat
//! dans le plafond, au lieu de s'y ajouter : consigne ≤ plafond, achat ≤ import du compteur.
void testOffPeakFullPowerAtDawnStaysInTheCeiling();
//! Lot e2, suite — pendant une bascule de phases, le plafond ne rend rien du courant de la borne.
void testOffPeakFullPowerGivesNothingBackWhileSwitchingPhases();
//! TOUTE méthode Get* du namespace rend ce que sa déclaration promet.
void testEveryGetterReturnsWhatItDeclares();
//! Le snapshot sert la DÉCISION : le canal, jamais la table — et l'absence n'est pas zéro.
void testTheSnapshotServesTheChannelNeverTheTable();
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();
// [Q7, suite] Le refus de SetLoadConfig PUBLIE sa cause (fault LOAD_CONFIG_REFUSED et
// faultParams), déclarée dans le schéma, sans rien modifier — mémoire, GetLoadConfig, disque.
void testSetLoadConfigRefusalIsPublished();
// [Q7, suite — relecture etm72] Une borne au nom de 300 caractères est AJOUTÉE, son libellé
// coupé à 256 caractères sans couper de paire de substitution ; un libellé hérité trop long
// sur une autre entrée ne bloque pas l'ajout.
void testAutoProvisionedLabelIsCutNeverRefused();
// [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();
// ── Lot délestage visible (2026-10-25) ──────────────────────────────────
//! La borne abaissée puis coupée par la protection de surcharge le DIT (OVERLOAD_THROTTLED),
//! le motif se lève au retour du cycle, et l'épisode fait UNE entrée du journal.
void testOverloadProtectionReasonOnTheCharger();
//! Une borne en mode manuel, hors de loads[] : l'épisode se lit au journal, s'ouvre et se
//! ferme par une notification qui n'a pas d'autre cause, et la reprise amont le referme.
void testOverloadProtectionOnAManualChargerIsLogged();
//! Deux mécanismes délestés par L4 (relais, SG-Ready) : un épisode de 20 min = une entrée
//! par charge, avant / après dans le langage de chaque mécanisme.
void testSheddingLogCoversEveryMechanism();
//! N + 1 épisodes : le plus ancien sort, les identifiants ne sont jamais réutilisés.
void testSheddingLogKeepsTheLastEpisodes();
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)