diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp index 2eb002d..f154a1e 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -422,10 +422,31 @@ bool EnergyArbitrator::evParticipatesInWaterfall(EvCharger *ev) const const ChargingInfo info = chargingInfo(ev->id()); - // Sans voiture assignée, le proxy ignorait la borne : ni SoC, ni plancher véhicule, ni - // échéance calculable. Rien de tout cela n'apparaît en entrant dans le waterfall. - if (!m_tm->findConfiguredThing(info.assignedCarId())) - return false; + // LM-1208-b — « pas de voiture assignée » N'EST PLUS un critère d'exclusion. + // + // Le critère venait du filtre d'entrée du proxy, où il avait un sens : sans voiture, le + // proxy n'a ni SoC, ni capacité, ni échéance à planifier, et il n'a donc rien à faire de + // cette borne. Le waterfall, lui, n'a besoin d'aucune des trois pour allouer du surplus — + // le rang et le plancher appartiennent à la BORNE (LM-1206), pas au véhicule. + // + // Trois raisons de le retirer, et la troisième est décisive : + // · le cas de l'INVITÉ le rend intenable — une voiture qui n'est pas celle du + // propriétaire ne sera jamais déclarée, et la borne cesserait d'être arbitrée + // précisément quand elle consomme (3,9 kW mesurés au banc le 2026-08-28) ; + // · le critère n'est pas VÉRIFIABLE — l'assignation est déclarée à la main, rien ne dit + // ce qui est réellement branché : un critère d'arbitrage assis sur une déclaration + // invérifiable n'arbitre pas, il devine ; + // · il CONTREDIT LM-1206, en réintroduisant la dépendance au véhicule que LM-1206 avait + // écartée. + // + // Ce qu'on perd sans voiture assignée est une FONCTION — l'échéance et la cible d'énergie, + // qui restent au proxy et que le proxy continue d'ignorer pour cette borne — et non + // l'arbitrage. Dégradation, pas exclusion : la même distinction que C1 de LM-1009. + // + // CONSÉQUENCE À TENIR AILLEURS : le manager ne PRÉPARE pas une borne sans voiture + // (prepareInformation() la filtre en entrée), donc son ChargingProcessInfo n'existe pas. + // C'est evTargetPhases() qui porte ce cas — sans quoi une borne triphasée serait annoncée + // monophasée et le budget divisé par trois. // Mode manuel : l'utilisateur pilote, l'arbitre s'abstient. Hors arbitrage — pas rang 0. if (info.chargingMode() == ChargingInfo::ChargingModeNormal) @@ -442,10 +463,10 @@ bool EnergyArbitrator::evParticipatesInWaterfall(EvCharger *ev) const // sert les financements RÉSEAU, teste `!ev->pluggedIn()` depuis toujours. Seul le chemin // waterfall ne le faisait pas. // - // Hors arbitrage, et non « servie à 0 avec un motif » : c'est la même famille que les deux - // cas ci-dessus (DESIGN_3g §4). Une borne débranchée ne consomme rien, sa consommation - // nulle est déjà au compteur, et lui donner un rang la ferait apparaître dans la cascade - // pour n'y rien faire. Le journal l'annonce, comme pour les deux autres. + // Hors arbitrage, et non « servie à 0 avec un motif » : c'est la même famille que le cas + // ci-dessus (DESIGN_3g §4). Une borne débranchée ne consomme rien, sa consommation nulle + // est déjà au compteur, et lui donner un rang la ferait apparaître dans la cascade pour + // n'y rien faire. Le journal l'annonce, comme pour l'autre. if (!ev->pluggedIn()) return false; @@ -496,6 +517,24 @@ uint EnergyArbitrator::evTargetPhases(EvCharger *ev, double referenceA) if (!ev) return 1; + // LM-1208-b — une borne SANS VOITURE ASSIGNÉE est arbitrée, mais le manager ne la prépare + // pas : prepareInformation() la filtre en entrée, donc elle n'a aucun ChargingProcessInfo. + // internalProcessInfo() rendrait alors un objet PAR DÉFAUT — canSwitchPhaseCount = false, + // maxPossiblePhaseCount = 1 — c'est-à-dire « monophasée » pour une borne qui peut être + // triphasée. Le budget serait divisé par trois, et EN SILENCE : le waterfall allouerait + // 3 680 W en croyant servir 16 A sur une phase, l'adaptateur commanderait 16 A, et le + // matériel en tirerait 11 040 W. C'est le défaut de 3g-1 pris à l'envers. + // + // Sans voiture, la capacité du véhicule est INCONNUE, et une valeur par défaut plausible + // est le pire des masques (LM-104). Deux conséquences, une seule décision : on ne BASCULE + // pas — basculer suppose de savoir ce que la voiture accepte, et executeChargingAction() + // ne commanderait de toute façon aucun desiredPhaseCount sans ChargingProcessInfo — et on + // retient le nombre de phases de la BORNE, qui est celui qu'elle utilisera réellement. + // C'est l'hypothèse conservatrice pour le budget : elle SURESTIME la consommation de la + // borne plutôt que de la sous-estimer. + if (!internalHasProcessInfo(ev)) + return qMax(1u, ev->phaseCount()); + // Les bornes de ce qui est POSSIBLE viennent du manager, qui les a établies ce cycle avec // la voiture branchée (canSwitchPhaseCount, min/maxPossiblePhaseCount). Les redériver ici // en ferait une seconde version — et c'est exactement ce qui a divergé en 3g-1 : viser @@ -618,16 +657,20 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const // --- loads[] : bornes VE (3g-1) ------------------------------------------------------ // Elles entrent désormais dans le MÊME contexte que les autres charges — c'est toute la - // transplantation. Le filtre d'entrée du proxy est repris à l'identique : une borne sans - // voiture ou en mode manuel est HORS arbitrage, pas servie en dernier. Sa consommation est - // déjà dans le compteur racine, donc déjà dans le budget ; lui allouer quoi que ce soit la - // compterait deux fois. + // transplantation. DEUX exclusions seulement, et elles ne disent pas la même chose : le + // mode manuel est une INTENTION (l'utilisateur pilote, l'arbitre s'abstient), l'absence de + // véhicule branché un CONSTAT (la borne ne peut rien tirer de ce qu'on lui allouerait). + // « Pas de voiture assignée » n'en est plus une — LM-1208-b, cf. evParticipatesInWaterfall(). + // + // Une borne exclue n'est pas pour autant sortie de la COMPTABILITÉ (LM-1208) : sa + // consommation est déjà dans le compteur racine, donc déjà retranchée du budget ; lui + // allouer quoi que ce soit la compterait deux fois. for (auto it = m_adapters.constBegin(); it != m_adapters.constEnd(); ++it) { EvCharger *ev = internalEvChargers().value(ThingId(it.key())); if (!evParticipatesInWaterfall(ev)) { qCDebug(dcNymeaEnergy()) << "[Arbitre] Borne" << (ev ? ev->name() : it.key()) - << "hors waterfall (pas de voiture assignée, mode manuel, " - "ou aucun véhicule branché)."; + << "hors waterfall (mode manuel ou aucun véhicule " + "branché)."; continue; } ctx.loads.append(it.value()->toLoadContext(now)); diff --git a/energyplugin/etm/energyarbitrator.h b/energyplugin/etm/energyarbitrator.h index 2eb973b..d929f2f 100644 --- a/energyplugin/etm/energyarbitrator.h +++ b/energyplugin/etm/energyarbitrator.h @@ -205,18 +205,29 @@ public: /*! * \brief [3g-1] Cette borne participe-t-elle au waterfall de surplus ? * - * Trois exclusions, et toutes disent la même chose — cette borne ne peut rien faire de ce - * qu'on lui allouerait : **pas de voiture assignée**, **mode manuel** - * (\c ChargingModeNormal), ou **aucun véhicule branché**. + * **Deux** exclusions, et elles ne disent pas la même chose : le **mode manuel** + * (\c ChargingModeNormal) est une INTENTION — l'utilisateur pilote, l'arbitre s'abstient ; + * **aucun véhicule branché** (\c pluggedIn) est un CONSTAT — la borne ne peut rien tirer + * de ce qu'on lui allouerait. * * Les exclure du contexte est plus juste que leur donner un rang : leur consommation est - * déjà dans le compteur racine, donc dans le budget, sans qu'on ait à la leur allouer. + * déjà dans le compteur racine, donc dans le budget, sans qu'on ait à la leur allouer + * (LM-1208 — sortir de la décision n'est pas sortir de la comptabilité). * - * \note **`pluggedIn` a été ajouté après le banc du 2026-08-27** : les deux premières - * exclusions venaient du filtre d'entrée du proxy, qui n'avait pas besoin de la - * troisième — son allocation n'était pas une cascade, donc gaspiller ne privait - * personne. En cascade, une borne débranchée absorbe le budget des charges suivantes - * (4 853 W mesurés, \c docs/RELEVE_3g2.md §3.1). + * \note **`pluggedIn` a été ajouté après le banc du 2026-08-27** : les exclusions + * d'origine venaient du filtre d'entrée du proxy, qui n'avait pas besoin de celle-ci — + * son allocation n'était pas une cascade, donc gaspiller ne privait personne. En + * cascade, une borne débranchée absorbe le budget des charges suivantes (4 853 W + * mesurés, \c docs/RELEVE_3g2.md §3.1). + * \note **« Pas de voiture assignée » a été RETIRÉ des exclusions le 2026-08-28** + * (LM-1208-b) : le critère contredisait LM-1206 — le rang et le plancher appartiennent à + * la borne, pas au véhicule — il n'était pas vérifiable, et il excluait la borne d'un + * invité au moment précis où elle consomme. Ce qu'on perd sans voiture assignée est une + * FONCTION (échéance, cible d'énergie, toutes deux restées au proxy), pas l'arbitrage. + * \warning Corollaire à ne pas perdre : une borne sans voiture n'a **pas** de + * \c ChargingProcessInfo — \c SmartChargingManager::prepareInformation() l'écarte + * toujours. Tout code qui lit \c internalProcessInfo() pour une borne du waterfall doit + * d'abord interroger \c internalHasProcessInfo() ; voir \c evTargetPhases(). */ bool evParticipatesInWaterfall(EvCharger *ev) const; @@ -234,6 +245,13 @@ public: * puisse jamais la démarrer. Le plafond est donc rendu ici, au seul endroit qui parle * de phases pour le waterfall. * + * \note **[LM-1208-b] Sans \c ChargingProcessInfo, la règle du manager n'existe pas** — + * c'est le cas d'une borne sans voiture assignée, que \c prepareInformation() écarte + * alors qu'elle est désormais arbitrée. La capacité du véhicule étant inconnue, on ne + * bascule pas et on retient le nombre de phases de la BORNE. Laisser + * \c internalProcessInfo() rendre son objet par défaut annoncerait « monophasée » une + * borne triphasée, et diviserait le budget du cycle par trois — silencieusement. + * * \param ev Borne concernée. * \param referenceA Courant de référence, en ampères TOTAUX toutes phases confondues — * voir \c evReferenceCurrentA(). Peut être négatif en soutirage. diff --git a/energyplugin/smartchargingmanager.h b/energyplugin/smartchargingmanager.h index e41c379..dccc2a2 100644 --- a/energyplugin/smartchargingmanager.h +++ b/energyplugin/smartchargingmanager.h @@ -123,7 +123,8 @@ signals: // Sémantique amont inchangée : NON documentés // ici, la doc appartient à l'amont. // 4. internalEvChargers(), internalChargingActions(), internalRootMeter(), -// internalProcessInfo() — accesseurs ETM neufs, documentés ci-dessous. +// internalProcessInfo(), internalHasProcessInfo() +// — accesseurs ETM neufs, documentés ci-dessous. // 5. bestPhaseCountFor(), phasesForCount() — passe-plats ETM vers deux règles amont // privées, documentés ci-dessous. // 7. setEvChargingState() — écriture ETM d'un état RUNTIME, documentée @@ -261,6 +262,26 @@ protected: * et sous surcharge cela commandait 3 × 6 A là où le proxy commandait 1 × 6 A. */ ChargingProcessInfo internalProcessInfo(EvCharger *evCharger) const { return m_processInfos.value(evCharger); } // [ETM] new + + /*! + * \brief [ETM · LM-1208-b] Cette borne a-t-elle été PRÉPARÉE ce cycle ? + * + * \param evCharger Borne. + * \return Vrai si \c prepareInformation() lui a construit un \c ChargingProcessInfo + * pendant ce cycle ; faux sinon — notamment quand aucune voiture ne lui est assignée, + * cas où le filtre d'entrée de \c prepareInformation() l'écarte. + * + * \note Existe parce que \c internalProcessInfo() ne sait pas dire « absent » : il rend un + * objet par DÉFAUT, dont les valeurs — 1 phase, pas de bascule — sont des réponses + * parfaitement plausibles qu'aucun appelant ne peut distinguer d'une préparation réelle. + * \note Le cas est devenu RÉEL avec **LM-1208-b** : une borne sans voiture assignée entre + * dans le waterfall, alors que \c prepareInformation() continue de l'écarter. Elle est + * donc arbitrée sans \c ChargingProcessInfo, et + * \c EnergyArbitrator::evTargetPhases() doit le SAVOIR plutôt que de le déduire de + * valeurs par défaut — sans quoi une borne triphasée est annoncée monophasée et le + * budget du cycle est divisé par trois. + */ + bool internalHasProcessInfo(EvCharger *evCharger) const { return m_processInfos.contains(evCharger); } // [ETM] new // [ETM] END private slots: diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index 952ef0f..3d6c645 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -1085,6 +1085,32 @@ que C1 de LM-1009 : une capacité manquante se dit, elle ne fait pas disparaîtr > personne n'a choisi un mode éco** — ce qui est correct par construction, mais doit être visible > à la mise en service, sans quoi « la borne ne charge pas toute seule » se lira comme un défaut. +**LM-1208-c — Sans voiture, le nombre de phases vient de la BORNE.** *(Conséquence de +LM-1208-b trouvée à la mise en œuvre, 2026-08-28. Elle n'est pas un détail d'implantation : elle +décide du budget.)* + +Retirer le critère ne suffit pas, et ce qui manque ne se voit pas. Le proxy amont **ne prépare +pas** une borne sans voiture — `prepareInformation()` la filtre en entrée, et c'est correct : +sans véhicule il n'a ni SoC, ni capacité, ni échéance à planifier. La borne entre donc dans le +waterfall **sans `ChargingProcessInfo`**, c'est-à-dire sans la règle de bascule 1↔3 phases que +LM-1206 avait pris soin de ne pas redériver. + +Or l'accesseur rend un objet **par défaut**, pas une absence — et ses valeurs par défaut sont +*plausibles* : une phase, pas de bascule. Une borne triphasée serait donc annoncée monophasée, +et **le budget divisé par trois** : plafond à 3 680 W au lieu de 11 040, courant commandé à 16 A +au lieu de 9, et 11 kW tirés d'une enveloppe qui en comptait 3,7. C'est le défaut de 3g-1 +(« viser 3 phases sur une combinaison qui n'en permet qu'une ») pris exactement à l'envers. + +**La règle** : sans `ChargingProcessInfo`, on **ne bascule pas** — basculer suppose de savoir ce +que la voiture accepte — et on retient `phaseCount` de la **borne**, qui est ce qu'elle utilisera +réellement. C'est l'hypothèse conservatrice au sens de LM-104 : elle **surestime** la +consommation de la borne plutôt que de la sous-estimer. + +> **Ce que ce cas apprend au-delà des bornes** : un accesseur qui rend un défaut là où il +> faudrait dire « absent » est un masquage de panne (corollaire LM-104), et il est d'autant plus +> dangereux que le défaut est crédible. La question « cette borne a-t-elle été préparée ? » doit +> être POSABLE, pas déduite d'une valeur. + **LM-1207 — Ne PAS ajouter de `priority` à `ChargingInfo`.** Ce serait un **second système de priorité**, et il serait immédiatement insuffisant : il ne saurait pas exprimer **« VE1 > ECS > VE2 »**, puisqu'il ne classerait que les bornes entre elles. Deux classements diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index 0844ae1..8d2c6f7 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -5506,3 +5506,123 @@ void Simulation::testUnpluggedChargerTakesNoBudget() QCOMPARE(qRound(tele.value(routeurId.toString()).value("allocatedW").toDouble()), 0); #endif } + +void Simulation::testChargerWithoutCarIsArbitrated() +{ +#ifndef ETM_ARBITRATOR + QSKIP("testChargerWithoutCarIsArbitrated nécessite ETM_ARBITRATOR."); +#else + // [LM-1208-b · banc 2026-08-28] Le cas de l'INVITÉ. Une borne en Eco, un véhicule branché, + // et AUCUNE voiture assignée — parce qu'une voiture qui n'est pas celle du propriétaire ne + // sera jamais déclarée. Elle tirait 3,9 kW au banc et l'arbitre l'ignorait : le critère + // « pas de voiture assignée » la sortait du waterfall précisément quand elle consommait. + // + // Deux choses sont éprouvées ici, et la seconde est celle qui se serait perdue : + // 1. la borne EST arbitrée sans voiture assignée ; + // 2. elle l'est avec le nombre de phases de la BORNE. Le manager ne prépare pas une + // borne sans voiture (prepareInformation la filtre), donc son ChargingProcessInfo + // n'existe pas — et le lire par défaut annonce « 1 phase » pour une borne triphasée. + // Le défaut serait SILENCIEUX : plafond à 3 680 W au lieu de 11 040, courant commandé + // à 16 A au lieu de 9, et 11 kW tirés d'un budget qui en comptait 3,7. + const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-nocar.json"; + QFile::remove(cfgPath); + qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8()); + + cleanupTestCase(); + m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite"; + initTestCase(); + + EnergyArbitrator *arb = dynamic_cast(m_experiencePlugin->smartChargingManager()); + QVERIFY(arb); + ThingManager *tm = NymeaCore::instance()->thingManager(); + + QUuid meterId = addMeter(); + m_experiencePlugin->energyManager()->setRootMeter(meterId); + Thing *meter = tm->findConfiguredThing(meterId); + QVERIFY(meter); + meter->setStateValue("connected", true); + + // Tolérance d'acquisition à 1,0 : aucun démarrage sous tolérance ne vient brouiller la + // lecture du plancher — EV_GRID_START a son propre test. + QCOMPARE(injectAndWait("NymeaEnergy.SetAcquisitionTolerance", + QVariantMap({{"acquisitionTolerance", 1.0}})).toMap() + .value("params").toMap().value("energyError").toString(), + QString("EnergyErrorNoError")); + + // La borne est TRIPHASÉE — c'est tout l'enjeu du point 2. Plancher 6 A × 230 V × 3 + // = 4 140 W, plafond 16 A × 230 V × 3 = 11 040 W. + QUuid borneId = addCharger("ABC", 16, 26657); + QUuid routeurId = addEtmVariableLoad(27050); + Thing *borne = tm->findConfiguredThing(borneId); + Thing *routeur = tm->findConfiguredThing(routeurId); + QVERIFY(borne && routeur); + armerBorne(borne); // branchée, à l'arrêt, 6 A + QCOMPARE(borne->stateValue("phaseCount").toUInt(), 3u); + routeur->setStateValue("currentPowerW", 0); + + // AUCUN assignedCarId — et aucune voiture n'est même ajoutée au système. + QVariantMap info; + info.insert("evChargerId", borneId); + info.insert("chargingMode", "ChargingModeEco"); + QCOMPARE(injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", info}})) + .toMap().value("params").toMap().value("energyError").toString(), + QString("EnergyErrorNoError")); + + const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0)); + meter->setStateValue("currentPower", 0); + arb->simulationCallUpdate(t0); // fait entrer la borne en configuration + QCoreApplication::processEvents(); + + // Une charge derrière la borne : sans elle, « la borne prend tout » ne se distinguerait pas + // de « personne ne prend rien ». + QVariantList l = injectAndWait("NymeaEnergy.GetLoadConfig").toMap() + .value("params").toMap().value("loadConfigs").toList(); + for (int i = 0; i < l.count(); ++i) { + QVariantMap m = l.at(i).toMap(); + if (m.value("id").toString() == borneId.toString()) m.insert("priority", 1); + l[i] = m; + } + l.append(QVariantMap{{"id", routeurId.toString()}, {"label", "Routeur PV"}, + {"adapter", "etmvariableload"}, {"mode", "dynamic"}, + {"maxPowerW", 3000}, {"priority", 2}, {"enabled", true}}); + QCOMPARE(injectAndWait("NymeaEnergy.SetLoadConfig", QVariantMap({{"loadConfigs", l}})) + .toMap().value("params").toMap().value("energyError").toString(), + QString("EnergyErrorNoError")); + + // ---- Sans voiture assignée : la borne est ARBITRÉE ------------------------------------ + meter->setStateValue("currentPower", -6000); + arb->simulationCallUpdate(t0.addSecs(60)); + QCoreApplication::processEvents(); + QHash tele = telemetrieParId(arb->loadTelemetry()); + + QVERIFY2(tele.contains(borneId.toString()), + "une borne sans voiture assignée a été exclue du waterfall (LM-1208-b)"); + + // Le budget entier lui revient : 6 000 W passent sous son plafond TRIPHASÉ (11 040 W). + // Annoncée monophasée, elle aurait plafonné à 3 680 W et laissé 2 320 W au routeur — c'est + // exactement la lecture qui distingue les deux. + QCOMPARE(qRound(tele.value(borneId.toString()).value("allocatedW").toDouble()), 6000); + QCOMPARE(qRound(tele.value(routeurId.toString()).value("allocatedW").toDouble()), 0); + + // Et le COURANT commandé le confirme au matériel, pas seulement dans la charge utile : + // 6 000 / (230 × 3) = 8,7 → 9 A. Sur une phase supposée, c'eût été 16 A écrêtés au plafond + // de la borne — soit 11 kW tirés d'un budget qui en comptait 3,7. + QCOMPARE(borne->stateValue("maxChargingCurrent").toUInt(), 9u); + + // ---- Le mode manuel, lui, RESTE une exclusion ------------------------------------------ + // Sans cette moitié, le test passerait aussi si toute borne était arbitrée sans condition : + // il ne prouverait pas que c'est le critère « voiture » qui a été retiré, et lui seul. + info.insert("chargingMode", "ChargingModeNormal"); + QCOMPARE(injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", info}})) + .toMap().value("params").toMap().value("energyError").toString(), + QString("EnergyErrorNoError")); + routeur->setStateValue("currentPowerW", 0); + arb->simulationCallUpdate(t0.addSecs(120)); + QCoreApplication::processEvents(); + tele = telemetrieParId(arb->loadTelemetry()); + + QVERIFY2(!tele.contains(borneId.toString()), + "une borne en mode manuel doit rester hors arbitrage"); + QCOMPARE(qRound(tele.value(routeurId.toString()).value("allocatedW").toDouble()), 3000); +#endif +} diff --git a/tests/auto/simulation/simulation.h b/tests/auto/simulation/simulation.h index 79128b8..454cd23 100644 --- a/tests/auto/simulation/simulation.h +++ b/tests/auto/simulation/simulation.h @@ -165,6 +165,13 @@ private slots: // 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(); + void printStates(Thing *thing); void updateChargerMeter(Thing *thing);