Patrick Schurig 0dc2a1aeba moteur : l'écart commande/mesure est jugé au début du cycle (P1)
L'écart était jugé APRÈS le dispatch, entre la consigne qui venait de
partir et la mesure de la précédente, puis consommé au cycle suivant
avant d'être rejugé. Toute marche de consigne suivie par la borne se
lisait donc comme un refus vieux d'un cycle (60 s, au-delà des 30 s /
7 s de la borne) : recrédit suspendu, coupure, et une boucle de trois
cycles sous un surplus constant (~1,5 kW exportés en moyenne).

jugerCommande() rend désormais UN jugement par cycle, au début du cycle,
lu par le recrédit et par la télémétrie :
- la commande EN VIGUEUR contre la mesure fraîche ; s'ils concordent
  (max(200 W, 15 %)), l'épisode se referme ;
- l'ouverture est datée à l'instant de la commande, en temps de cycle,
  et non plus à l'horloge murale de la dernière publication ;
- le sens est celui que la commande demande à la charge, et non plus
  deviné au niveau de la mesure ;
- en montée seulement, une mesure rapprochée de la commande de plus de
  200 W depuis le cycle précédent est une latence. La descente n'a
  jamais cette excuse.

command.divergent publie ce verdict ; expectedW et observedW restent
les nombres de la publication.

Le test de reproduction inverse ses assertions « DÉFAUT » : sur trois
rampes (instantanée, 30 s / 7 s, 120 s), 13 A tenus dès le premier
cycle, aucune suspension, export de 10 W dès que la borne a rejoint sa
consigne.

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

390 lines
23 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 banc 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();
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)