From 2a7bb1cce9754e713db7317fe6b73b11cbc43059 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 29 Aug 2026 13:27:25 +0200 Subject: [PATCH] =?UTF-8?q?fix(ECS-111):=20un=20relais=20disparu=20fige=20?= =?UTF-8?q?la=20charge=20au=20lieu=20de=20publier=20un=20fant=C3=B4me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le comportement était « non défini » depuis l'écriture de la spec. telemetry() SAUTAIT un relais qui ne résout plus : ou bien plus aucun relais actif ne mesurait et l'on retombait sur le nominal du PALIER ENTIER — part du fantôme comprise — ou bien des survivants mesuraient et l'on publiait une somme sous-évaluée. Et ce n'était pas de l'affichage : telemetry().currentPowerW alimente le recrédit anti-clignotement, donc le BUDGET. L'arbitre se trompait sur le surplus disponible pendant qu'available restait vrai. Un mensonge qui décide, pas un mensonge qui s'affiche. Option (a), la seule cohérente avec la famille : available faux, faultCode THING_MISSING, traitement ECS-410 — sortie de l'ARBITRAGE, maintien dans la COMPTABILITÉ, figée à la puissance crue engagée. La puissance est donc RETIRÉE du budget des suivantes au lieu de leur être recréditée. Aucune somme partielle. (b) — compter 0 W et servir le reste — est écartée : un relais absent du registre est un Thing supprimé ou un plugin qui ne le voit plus, on ne sait pas dans quel état est le contact, et servir le reste reviendrait à supposer. Jugé sur TOUS les relais déclarés, jamais sur ceux du palier courant — sinon la disponibilité basculerait au rythme du thermostat. Même règle que deviceMeasurement(), qui traitait DÉJÀ le cas correctement : aligner l'ancien sur le neuf, pas inventer une politique. THING_MISSING prime sur WRITE_FAILED : un relais absent EXPLIQUE les écritures qui échouent, et annoncer WRITE_FAILED enverrait l'opérateur au bus quand la cause est dans la configuration. D'où le corollaire, écrit dans la doc de clearFault : ClearLoadFault lève le défaut d'ÉCRITURE, il ne fait pas exister un Thing. testEcsPartialFailure a dû être ajusté et c'est instructif : il utilisait un relais JAMAIS CONFIGURÉ comme raccourci pour forcer un échec d'écriture. Le raccourci a maintenant un sens propre, et le test distingue les deux causes — le défaut collant se lève, le câblage manquant non. Trouvé ni par un test ni par le banc, mais par la LECTURE d'une règle que l'index a rendue visible. Consigné comme tel dans spec_ecs.md et AGENTS.md : c'est le premier défaut réel exhumé par le chantier de documentation. simulation 33/33, charging 16/16, loadmodel 20/20, amd64 0/0, doxygen 0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p --- AGENTS.md | 8 ++ energyplugin/etm/adapters/relayrouter.cpp | 53 ++++++++++- energyplugin/etm/adapters/relayrouter.h | 14 +++ specs/spec_ecs.md | 50 +++++++++- tests/auto/simulation/simulation.cpp | 109 +++++++++++++++++++++- tests/auto/simulation/simulation.h | 6 ++ 6 files changed, 231 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d413c74..122a998 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -356,6 +356,14 @@ Ensuite : **passe contrats** (OPTIMIZER_PROTOCOL + README). > Le **trou de `pluggedIn`** trouvé au même passage est CORRIGÉ — une borne sans véhicule > branché est hors arbitrage, elle n'absorbe plus le budget de la cascade. +> **Le chantier de documentation a exhumé son premier défaut réel** (2026-08-29). `ECS-111` — +> « on publie la puissance d'un relais disparu » — dormait dans une table de statut supprimée +> depuis, et est ressortie dans les quatorze règles « spécifiées seules en territoire livré » +> que `tools/gen-rules.py` a listées. La lecture a montré que le chiffre fantôme n'était pas +> seulement affiché : il alimentait le **recrédit**, donc le **budget**, pendant qu'`available` +> restait vrai. Ni un test ni le banc ne l'avaient vu. **Corrigé le jour même** (`ECS-111`, +> option (a) — sortie de l'arbitrage, maintien dans la comptabilité). + **Restent à guetter au banc, jamais à fabriquer** : `EV_GRID_START` (fenêtre `[plancher × 0,5 ; plancher[`), `BATTERY_RESERVE` avec un surplus réel sous le seuil, et LM-1204-b avec **deux bornes réelles** — la Terra AC ne répond toujours pas à l'ARP. diff --git a/energyplugin/etm/adapters/relayrouter.cpp b/energyplugin/etm/adapters/relayrouter.cpp index d059176..3994a5a 100644 --- a/energyplugin/etm/adapters/relayrouter.cpp +++ b/energyplugin/etm/adapters/relayrouter.cpp @@ -131,12 +131,36 @@ LoadTelemetry RelayRouter::telemetry() const // Un thermostat coupé (relais ON mais 0 W) → 0, jamais de puissance fantôme. // - NOMINAL commandé (repli) si aucun relais actif ne mesure (GPIO bool nu). Dans ce cas // currentPowerW reflète le COMMANDÉ, pas le mesuré (contrat rév. 3 §3.a). + // \rule{ECS-111} — UN RELAIS DÉCLARÉ QUI MANQUE REND LA CHARGE INDISPONIBLE. + // + // Le comportement était « non défini », et son effet dépassait l'affichage. Le relais + // disparu était SAUTÉ : ou bien plus aucun relais actif ne mesurait, et l'on retombait sur + // le nominal du PALIER ENTIER — la part du relais fantôme comprise ; ou bien des survivants + // mesuraient, et l'on publiait une somme SOUS-ÉVALUÉE. Dans les deux cas le chiffre était + // faux, et il n'était pas seulement affiché : \c currentPowerW alimente le recrédit + // anti-clignotement, donc le BUDGET. L'arbitre se trompait sur le surplus disponible + // pendant que \c available restait vrai — un mensonge qui décide, pas un mensonge qui + // s'affiche. + // + // Un relais absent du registre nymea est un Thing supprimé, ou un plugin qui ne le voit + // plus : **on ne sait pas dans quel état est le contact**. Compter 0 W et servir le reste + // serait supposer. ECS-410 dit quoi faire d'une charge dont on ne sait plus rien — sortir + // de l'ARBITRAGE, rester dans la COMPTABILITÉ, figer à la puissance crue engagée — et + // c'est le traitement appliqué ici, comme \c EtmVariableLoadAdapter et \c EvAdapter le + // font déjà pour leur Thing. \c deviceMeasurement() le faisait déjà : ceci finit le + // mouvement au lieu d'inventer une politique. + const bool relaisManquant = aUnRelaisManquant(); + t.available = t.available && !relaisManquant; + double power = 0; bool metered = false; const QList activeRelays = m_currentStage < m_relayMapping.size() ? m_relayMapping.at(m_currentStage) : QList(); - for (const QString &thingId : activeRelays) { + // Câblage incomplet : aucune somme partielle. On annonce le NOMINAL du palier courant — + // la plus haute puissance qui puisse couler, jamais moins (même direction qu'ECS-410 et + // ECS-411). Sous-estimer ferait offrir aux charges suivantes un budget qui n'existe pas. + for (const QString &thingId : relaisManquant ? QList() : activeRelays) { Thing *relay = m_thingManager->findConfiguredThing(ThingId(thingId)); if (!relay) continue; @@ -182,12 +206,21 @@ LoadContext RelayRouter::toLoadContext(const QDateTime &now) const // internes en puissances via la table de paliers. Sans ce canal, le scheduler décrémente // le budget d'une valeur que l'adaptateur va écrêter, et les charges suivantes reçoivent // un résidu surestimé pendant toute la fenêtre minOn (AGENTS règle 4). - ctx.telemetry.available = !m_faulted; + // \rule{ECS-111} — la disponibilité se lit sur telemetry(), PAS sur m_faulted seul : un + // relais déclaré qui manque au registre rend la charge indisponible au même titre qu'un + // défaut d'écriture. Recopier « !m_faulted » ici ferait qu'une charge annoncée + // indisponible en télémétrie continuerait d'être ALLOUÉE par le waterfall — les deux + // frontières diraient deux choses de la même charge. + ctx.telemetry.available = telemetry().available; - if (m_faulted) { + if (!ctx.telemetry.available) { // ECS-410 — charge FIGÉE, ni montée ni descente : plancher ET plafond à la puissance // crue engagée. Un plafond nul avec un plancher non nul serait contradictoire et // ferait dérailler le qBound du scheduler. + // + // C'est ce qui rend le traitement d'ECS-111 juste POUR LE BUDGET : la puissance crue + // engagée est RETIRÉE du budget des charges suivantes au lieu de leur être recréditée. + // Avant, le fantôme gonflait le recrédit — il offrait du surplus qui n'existait pas. ctx.telemetry.lockMinPowerW = ctx.telemetry.currentPowerW; ctx.telemetry.lockMaxPowerW = ctx.telemetry.currentPowerW; return ctx; @@ -518,6 +551,14 @@ void RelayRouter::settleTransition() } } +bool RelayRouter::aUnRelaisManquant() const +{ + for (auto it = m_relayNominalW.constBegin(); it != m_relayNominalW.constEnd(); ++it) + if (!m_thingManager->findConfiguredThing(ThingId(it.key()))) + return true; + return false; +} + LoadMeasurement RelayRouter::deviceMeasurement() const { LoadMeasurement m; @@ -580,6 +621,12 @@ LoadRuntimeView RelayRouter::runtimeView(const QDateTime &now) const // épuisée. Une cause nouvelle vaudra un code nouveau, jamais une phrase. if (m_faulted) v.faultCode = QStringLiteral("WRITE_FAILED"); + // \rule{ECS-111} — THING_MISSING PRIME sur WRITE_FAILED, et ce n'est pas un choix de + // présentation : un relais absent EXPLIQUE les écritures qui échouent. Annoncer + // WRITE_FAILED enverrait l'opérateur au bus alors que la cause est dans la configuration, + // et \c ClearLoadFault — le remède de WRITE_FAILED — n'y peut rien. + if (aUnRelaisManquant()) + v.faultCode = QStringLiteral("THING_MISSING"); // Verrou en SECONDES RESTANTES. Le calcul dérive du MÊME m_lastSwitch et du même \c now que // lockWindow() : ce que le client lit est ce qui a borné la décision, pas une seconde vue. diff --git a/energyplugin/etm/adapters/relayrouter.h b/energyplugin/etm/adapters/relayrouter.h index fbe705b..50c915c 100644 --- a/energyplugin/etm/adapters/relayrouter.h +++ b/energyplugin/etm/adapters/relayrouter.h @@ -180,6 +180,11 @@ public: /*! * \brief Lève le verrou de défaut (ECS-410, RPC \c NymeaEnergy.ClearLoadFault). * \return Vrai si un défaut a RÉELLEMENT été levé, faux s'il n'y en avait pas. + * \note **Lever le défaut ne rend pas forcément la charge disponible.** \rule{ECS-111} : + * un relais déclaré qui manque au registre la garde indisponible, et ClearLoadFault n'y + * peut rien — il ne fait pas exister un Thing. Les deux causes sont distinctes et se + * lisent séparément : \c faulted() pour le défaut d'écriture collant, + * \c telemetry().available pour l'aptitude réelle à être commandée. * \note Le défaut est COLLANT par conception — « cesser toute commande jusqu'à * intervention ». Sa levée est donc un acte délibéré et tracé de l'opérateur, jamais * une expiration. L'état matériel réel étant inconnu après un défaut, il est RELU @@ -252,6 +257,15 @@ private: int deduceStageFromThings() const; void applyRelayStage(int stage); + /*! + * \brief Un relais DÉCLARÉ manque-t-il au registre nymea ? \rule{ECS-111} + * \return Vrai dès qu'un seul \c thingId déclaré ne résout plus. + * \note Jugé sur TOUS les relais déclarés, jamais sur ceux du palier courant : au palier 0 + * aucun n'est fermé, et juger là-dessus ferait basculer la disponibilité au rythme du + * thermostat. Même règle que \c deviceMeasurement(). + */ + bool aUnRelaisManquant() const; + ThingManager *m_thingManager; QString m_id; QString m_label; diff --git a/specs/spec_ecs.md b/specs/spec_ecs.md index 7399477..e5ece16 100644 --- a/specs/spec_ecs.md +++ b/specs/spec_ecs.md @@ -712,11 +712,51 @@ Les identifiants sont normalisés via `QUuid` : la configuration mélange `{uuid charge en double sont refusés par la même passe — la seconde entrée était jusqu'ici perdue en silence à la construction. -**ECS-111 — Suppression d'un Thing référencé.** Comportement non défini -aujourd'hui : `findConfiguredThing` renvoie `nullptr`, `writeRelay` logue et -continue, `telemetry()` saute le relais donc `metered = false` et l'on retombe -sur le nominal commandé — **on publie la puissance d'un relais disparu**. À -définir et tester. +**ECS-111 — Suppression d'un Thing référencé. DÉFINI ET CORRIGÉ le 2026-08-29.** + +*Le défaut, tel qu'il était.* `findConfiguredThing` renvoyait `nullptr`, `writeRelay` loguait et +continuait, `telemetry()` **sautait** le relais. Deux issues, toutes deux fausses : ou bien plus +aucun relais actif ne mesurait et l'on retombait sur le nominal du **palier entier** — part du +relais fantôme comprise ; ou bien des survivants mesuraient et l'on publiait une somme +**sous-évaluée**. + +> **Et ce n'était pas un défaut d'affichage.** `telemetry().currentPowerW` alimente le **recrédit +> anti-clignotement** (`rulebasedscheduler.cpp`), donc le **budget**. L'arbitre se trompait sur le +> surplus disponible pendant qu'`available` restait vrai. **Un mensonge qui décide, pas un +> mensonge qui s'affiche** — c'est ce qui a fait passer ce point d'« à définir » à corrigé le jour +> même. + +**La règle.** Un relais **déclaré** qui manque au registre nymea rend la charge **indisponible** : +`available: false`, `faultCode: THING_MISSING`, et le traitement d'ECS-410 s'applique — **sortie de +l'ARBITRAGE, maintien dans la COMPTABILITÉ**, figée à la puissance crue engagée. Aucune somme +partielle n'est jamais publiée. + +*Pourquoi pas « compter 0 W et servir le reste ».* Un relais absent du registre est un Thing +supprimé, ou un plugin qui ne le voit plus : **on ne sait pas dans quel état est le contact**. +Servir le reste reviendrait à commander une charge dont on ne connaît plus la configuration +matérielle — c'est-à-dire à supposer. + +> **Jugé sur TOUS les relais déclarés**, jamais sur ceux du palier courant : au palier 0 aucun +> n'est fermé, et juger là-dessus ferait basculer la disponibilité au rythme du thermostat. Même +> règle que `deviceMeasurement()` (LM-1105) — qui, lui, traitait **déjà** le cas correctement en +> rendant `source: "none"`. Aligner l'ancien sur le neuf n'a pas été inventer une politique, mais +> finir un mouvement. +> +> ⚠️ **`ClearLoadFault` ne rend pas la charge disponible pour autant.** Il lève le défaut +> d'**écriture** — c'est sa promesse — mais ne fait pas exister un Thing. Les deux causes se +> lisent séparément : `faulted()` pour le défaut collant, `available` pour l'aptitude réelle à +> être commandée. + +*Éprouvé par* `testVanishedRelayFreezesTheLoad`, **vérifié échouant** sans le correctif, et dont +la moitié qui compte est la troisième assertion : ce que le fantôme laissait aux charges +**suivantes**. + +> **Comment ce défaut a été trouvé.** Ni par un test, ni par le banc : **par la lecture d'une +> règle que l'index a rendue visible.** `ECS-111` dormait dans une table de statut supprimée +> depuis, puis est ressortie dans les quatorze règles « spécifiées seules en territoire livré » +> qu'`tools/gen-rules.py` a listées. C'est le premier défaut réel exhumé par le chantier de +> documentation — et l'argument le plus concret pour le poursuivre : un index qui rend les +> règles lisibles finit par rendre les trous lisibles aussi. --- diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index 3450cf3..57fd552 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -1558,7 +1558,18 @@ void Simulation::testEcsPartialFailure() qInstallMessageHandler(s_handlerPrecedent); QVERIFY(leve); QVERIFY(!r->faulted()); - QCOMPARE(r->telemetry().available, true); + + // [ECS-111, 2026-08-29] La charge reste INDISPONIBLE, et c'est la bonne réponse : ce + // routeur déclare `rKo`, un relais JAMAIS configuré. ClearLoadFault lève le défaut + // d'ÉCRITURE — c'est sa promesse, et `!r->faulted()` juste au-dessus la vérifie — mais il + // ne fait pas exister un Thing. Annoncer `available: true` ici serait vrai pendant un + // cycle et faux au suivant, la première écriture rouvrant le défaut. + // + // Ce test se servait d'un relais inexistant comme RACCOURCI pour forcer un échec + // d'écriture. Le raccourci a désormais un sens propre — câblage incomplet — et les deux + // causes se distinguent : le défaut collant se lève, le câblage manquant non. + QCOMPARE(r->telemetry().available, false); + QCOMPARE(r->runtimeView(t0).faultCode, QStringLiteral("THING_MISSING")); const QString issue1 = s_journal.filter(QStringLiteral("ClearLoadFault")).join(QStringLiteral(" | ")); QVERIFY2(issue1.contains(QStringLiteral("défaut LEVÉ")), qUtf8Printable(issue1)); @@ -5985,3 +5996,99 @@ void Simulation::testDeletedEvChargerConfigComesBack() QCOMPARE(apres, avant); #endif } + +void Simulation::testVanishedRelayFreezesTheLoad() +{ +#ifndef ETM_ARBITRATOR + QSKIP("testVanishedRelayFreezesTheLoad nécessite ETM_ARBITRATOR."); +#else + // [ECS-111] Le comportement était « non défini », et son effet dépassait l'affichage : le + // relais disparu était SAUTÉ, on retombait sur le nominal du PALIER ENTIER — part du relais + // fantôme comprise — et ce chiffre alimente le recrédit anti-clignotement, donc le BUDGET. + // L'arbitre se trompait sur le surplus pendant que `available` restait vrai. + // + // Ce test porte les deux moitiés, et c'est la seconde qui compte : ce que le fantôme + // laissait aux charges SUIVANTES. + const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-vanish.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); + + QUuid rA = addPowerSwitch(2000, 26661); // ECS : un seul relais, 2000 W + QUuid varId = addEtmVariableLoad(27043); // la charge SUIVANTE, celle qui paie + Thing *routeur = tm->findConfiguredThing(varId); + QVERIFY(tm->findConfiguredThing(rA) && routeur); + routeur->setStateValue("currentPowerW", 0); + + QVariantList l; + l.append(QVariantMap{{"id", "ecs-fantome"}, {"label", "ECS relais"}, + {"adapter", "relay-router"}, {"mode", "fixed"}, + {"priority", 1}, {"enabled", true}, + {"relays", QVariantList() << QVariantMap{{"thingId", rA.toString()}, + {"powerW", 2000}}}, + {"minOnS", 0}, {"minOffS", 0}}); + l.append(QVariantMap{{"id", varId.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")); + + // ---- Régime sain : l'ECS prend son palier, le routeur prend le reste ------------------ + const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0)); + meter->setStateValue("currentPower", -5000); + arb->simulationCallUpdate(t0); + QCoreApplication::processEvents(); + QHash tele = telemetrieParId(arb->loadTelemetry()); + QCOMPARE(tele.value("ecs-fantome").value("available").toBool(), true); + QCOMPARE(qRound(tele.value("ecs-fantome").value("allocatedW").toDouble()), 2000); + const int routeurSain = qRound(tele.value(varId.toString()).value("allocatedW").toDouble()); + QVERIFY2(routeurSain > 0, "le routeur doit être servi en régime sain"); + + // ---- Le relais DISPARAÎT du registre nymea -------------------------------------------- + // Thing supprimé, ou plugin qui ne le voit plus : on ne sait pas dans quel état est le + // contact. C'est précisément pour ça qu'on ne peut pas « compter 0 W et servir le reste ». + QVERIFY(removeDevice(rA).toMap().value("params").toMap().value("thingError").toString() + == "ThingErrorNoError"); + arb->simulationCallUpdate(t0.addSecs(60)); + QCoreApplication::processEvents(); + tele = telemetrieParId(arb->loadTelemetry()); + + // 1. La charge SORT DE L'ARBITRAGE, et elle le DIT. + const QVariantMap ecs = tele.value("ecs-fantome"); + QCOMPARE(ecs.value("available").toBool(), false); + QCOMPARE(ecs.value("faultCode").toString(), QString("THING_MISSING")); + QCOMPARE(ecs.value("decision").toMap().value("code").toString(), + QString(DecisionCode::LoadUnavailable)); + + // 2. …mais PAS de la comptabilité : la puissance crue engagée reste allouée, parce qu'on + // ne sait plus la couper. C'est ECS-410, appliqué à une cause nouvelle. + QCOMPARE(qRound(ecs.value("allocatedW").toDouble()), 2000); + + // 3. ET C'EST LA MOITIÉ QUI DÉCIDE : le fantôme est RETIRÉ du budget des suivantes au lieu + // de leur être recrédité. Avant le correctif, `currentPowerW` valait le nominal du + // palier entier et alimentait le recrédit — le routeur se voyait offrir un surplus qui + // n'existait pas. Ici il reçoit ce qui reste vraiment, pas davantage. + const int routeurFantome = qRound(tele.value(varId.toString()).value("allocatedW").toDouble()); + QVERIFY2(routeurFantome <= routeurSain, + "la charge suivante ne doit pas recevoir PLUS quand un relais a disparu"); + + // 4. Et la mesure ne prétend rien : rien ne mesure une charge dont le câblage manque. + QCOMPARE(ecs.value("measurement").toMap().value("source").toString(), QString("none")); + QVERIFY2(!ecs.value("measurement").toMap().contains("powerW"), + "une charge au câblage incomplet ne doit pas publier de puissance mesurée"); +#endif +} diff --git a/tests/auto/simulation/simulation.h b/tests/auto/simulation/simulation.h index 23525b7..6b9369c 100644 --- a/tests/auto/simulation/simulation.h +++ b/tests/auto/simulation/simulation.h @@ -191,6 +191,12 @@ private slots: // 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);