diff --git a/docs/INVENTAIRE_MOCK.md b/docs/INVENTAIRE_MOCK.md index 22fffd8..fdc1eaf 100644 --- a/docs/INVENTAIRE_MOCK.md +++ b/docs/INVENTAIRE_MOCK.md @@ -62,9 +62,16 @@ ne bouge. > conclusion tenait, la raison non**, et rien ne pouvait le révéler tant que le déséquilibre était > infabricable. -**3. `temperature`.** Le code teste `hasState("temperature")` et **aucune classe du mock ne le -porte** : la branche « sonde présente » n'a jamais été exécutée. Elle est écrite, elle n'est pas -testée. +**3. ~~`temperature`.~~ — OUVERT le 2026-09-05.** La classe `thermalProbe` porte l'état, et +`testThermalProbeFeedsTelemetry` exerce enfin la branche « sonde présente » de +`attachMeasurements()`, écrite depuis toujours et **jamais parcourue**. C'était la seule entrée du +volet thermique, et rien ne l'exerçait. + +> **Ce qu'il reste à dire, et qui n'est pas corrigé ici** : une sonde DÉSIGNÉE mais incapable — +> un Thing sans état `temperature` — produit exactement la même charge utile qu'aucune sonde du +> tout (`temperatureC` absent). Le geste de réparation, lui, diffère : désigner une sonde, ou en +> changer. C'est un silence de la famille `progressUnmeasurableCause`, relevé par le test et +> laissé ouvert : le fermer demande un catalogue de causes thermiques qui n'existe pas encore. **4. ~~Une triphasée déséquilibrée.~~ — OUVERT le 2026-09-03.** `testWorstPhaseBindsWhenPhasesAreUnbalanced` exerce enfin `LM-1210-a` : phase B saturée à 30 A @@ -105,12 +112,35 @@ monotone — le mock fabriquerait l'anomalie que le moteur est censé détecter. **Quatrième occurrence.** Après les six états cohérents du `rootmeter`, les classes portant toutes une puissance, et la triphasée équilibrée. +**6. ~~Le compteur sans énergie et la sonde thermique.~~ — OUVERTS le 2026-09-05**, et les deux +derniers de la liste. `meterNoEnergy` remplace le `powerSwitch` détourné en instrument de mesure — +un montage que personne ne fait, et qui faisait tenir DEUX rôles à la même Thing dans le même +scénario. `thermalProbe` ouvre le volet thermique. + +> **Et l'ouverture a révélé un défaut, comme les trois précédentes.** `setupThing()` du mock +> n'avait **aucune sortie par défaut** : une classe sans branche ne rencontrait jamais de +> `info->finish()`. Le setup ne échouait pas — **il ne se terminait jamais**. Le symptôme est un +> Thing éternellement « en cours de configuration » et un test qui expire ailleurs, très loin de +> la cause. +> +> C'est le même mécanisme que le contrôleur déréférencé pour toute classe : **une hypothèse du +> mock que son homogénéité protégeait.** Tant que chaque classe avait sa branche, l'absence de +> sortie par défaut ne pouvait pas se voir. Corrigé : une classe oubliée se dit maintenant en +> `qCWarning` et rend `ThingErrorSetupFailed`. +> +> **Et une garde a mordu au passage** : la première version du test thermique lisait `loads[]` +> sans compteur racine — donc une liste vide, et trois assertions qui ne se seraient jamais +> exécutées. `QCOMPARE(parId.count(), 3)` l'a attrapé. C'est la parade écrite plus haut dans ce +> document, appliquée le jour même à son auteur. + ## Ce qu'il faudrait ajouter, par ordre d'utilité 1. **Une classe pilotable SANS aucune puissance** — un relais sec. Débloque `source: none` sur une charge saine, et sépare enfin « non mesurée » de « en panne ». -2. **Un compteur sans `totalEnergyConsumed`** en tant que classe déclarée, plutôt qu'un - `powerSwitch` détourné comme aujourd'hui. +2. ~~**Un compteur sans `totalEnergyConsumed`** en tant que classe déclarée.~~ — **FAIT le + 2026-09-05** : `meterNoEnergy`. Elle ne déclare PAS l'interface `energymeter`, qui exige + `totalEnergyConsumed` — promettre une interface qu'on ne tient pas ferait mentir le mock là où + il doit être littéral. 3. **Une tension par phase réglable** indépendamment des puissances — pour exercer le correctif amont ailleurs qu'en inversant la fonction. diff --git a/tests/auto/common/energytestbase.cpp b/tests/auto/common/energytestbase.cpp index f6e8c07..81d20d1 100644 --- a/tests/auto/common/energytestbase.cpp +++ b/tests/auto/common/energytestbase.cpp @@ -501,6 +501,38 @@ QUuid EnergyTestBase::addDryRelay(quint16 port) return response.toMap().value("params").toMap().value("thingId").toUuid(); } +QUuid EnergyTestBase::addMeterNoEnergy(quint16 port) +{ + QVariantMap portParam; + portParam.insert("paramTypeId", "{eec1f5a2-5c59-4ce9-a5ae-5dc7cad70772}"); + portParam.insert("value", port); + + QVariantMap params; + params.insert("thingClassId", mockMeterNoEnergyThingClassId.toString()); + params.insert("name", QString("Meter without energy %1").arg(port)); + params.insert("thingParams", QVariantList() << portParam); + + QVariant response = injectAndWait("Integrations.AddThing", params); + verifyThingError(response); + return response.toMap().value("params").toMap().value("thingId").toUuid(); +} + +QUuid EnergyTestBase::addThermalProbe(quint16 port) +{ + QVariantMap portParam; + portParam.insert("paramTypeId", "{b898397c-c291-47f9-82ec-662250b932e1}"); + portParam.insert("value", port); + + QVariantMap params; + params.insert("thingClassId", mockThermalProbeThingClassId.toString()); + params.insert("name", QString("Thermal probe %1").arg(port)); + params.insert("thingParams", QVariantList() << portParam); + + QVariant response = injectAndWait("Integrations.AddThing", params); + verifyThingError(response); + return response.toMap().value("params").toMap().value("thingId").toUuid(); +} + QUuid EnergyTestBase::addPowerSwitch(double nominalPower, quint16 port) { QVariantList thingParams; diff --git a/tests/auto/common/energytestbase.h b/tests/auto/common/energytestbase.h index 2b4fdf3..ea6831a 100644 --- a/tests/auto/common/energytestbase.h +++ b/tests/auto/common/energytestbase.h @@ -59,6 +59,8 @@ static QUuid mockPowerSwitchThingClassId = QUuid("841f8905-d1d7-4053-909f-01123b //! puissance, si bien qu'aucun test ne pouvait distinguer « non mesurée » de « en défaut » //! (docs/INVENTAIRE_MOCK.md). static QUuid mockDryRelayThingClassId = QUuid("5b2c7e41-9a06-4d3f-b8c1-6e0f2a7d4b39"); +static QUuid mockMeterNoEnergyThingClassId = QUuid("50dacc7b-7142-4716-8f59-97d6ee81f39c"); +static QUuid mockThermalProbeThingClassId = QUuid("37e61ea3-e4b2-4d3e-bc82-66e350c29f66"); using namespace nymeaserver; @@ -118,6 +120,25 @@ public: * ce qui traîne `available: false` et mélange deux affirmations distinctes. */ QUuid addDryRelay(quint16 port = 26680); + + /*! + * \brief Ajoute un COMPTEUR SANS CUMUL D'ÉNERGIE : il publie une puissance, il ne totalise rien. + * \param port Port du contrôleur mock. + * \return L'identifiant du Thing créé. + * \note Remplace le détournement d'un \c powerSwitch en rôle de compteur — un interrupteur + * dans le rôle d'un instrument, et la même Thing tenant deux rôles dans le même scénario. + * C'est le cas `meterWithoutEnergy`, mesuré sur l'ECS du banc le 2026-09-01. + */ + QUuid addMeterNoEnergy(quint16 port = 26671); + + /*! + * \brief Ajoute une SONDE DE TEMPÉRATURE — la seule entrée du volet thermique. + * \param port Port du contrôleur mock. + * \return L'identifiant du Thing créé. + * \note Aucune classe du mock ne portait l'état \c temperature : la branche « sonde + * présente » de \c attachMeasurements() était écrite et n'avait jamais été exécutée. + */ + QUuid addThermalProbe(quint16 port = 26681); //! Mock thing implémentant l'interface etmvariableload (states currentPowerW / powerSetpoint). QUuid addEtmVariableLoad(quint16 port = 27001); diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index 0d6810d..ccc232d 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -7419,8 +7419,9 @@ void Simulation::testRecreditIsALoanAgainstObedience() * * \par Et `meterThingId` ne permet pas de le déduire * Un compteur peut publier `currentPower` **sans** `totalEnergyConsumed` : mesuré le 2026-09-01 - * sur l'ECS du banc, et reproduit ici avec un `powerSwitch` désigné comme compteur. Un client qui - * déduirait la mesurabilité de la présence du champ se tromperait sur ce cas précis. + * sur l'ECS du banc, et reproduit ici par la classe `meterNoEnergy` — un compteur DÉCLARÉ tel, + * et non plus un `powerSwitch` détourné en instrument de mesure. Un client qui déduirait la + * mesurabilité de la présence du champ se tromperait sur ce cas précis. */ void Simulation::testProgressMeasurabilityIsDeclaredAtConfigTime() { @@ -7436,10 +7437,25 @@ void Simulation::testProgressMeasurabilityIsDeclaredAtConfigTime() ThingManager *tm = NymeaCore::instance()->thingManager(); QUuid compteur = addMeter(26702); // meter : porte totalEnergyConsumed - QUuid r1 = addPowerSwitch(1000, 26703); // powerSwitch : power + currentPower SEULEMENT + QUuid r1 = addPowerSwitch(1000, 26703); QUuid r2 = addPowerSwitch(1000, 26704); QUuid r3 = addPowerSwitch(1000, 26705); + // COMPTEUR SANS CUMUL, classe déclarée — plus un `powerSwitch` détourné en instrument de + // mesure. Le détour marchait (le moteur ne teste que `hasStateType`), mais il modélisait un + // montage que personne ne fait, et faisait tenir DEUX rôles à la même Thing dans le même + // scénario : relais de « sans-compteur » et compteur de « compteur-sans-energie ». + QUuid compteurSansEnergie = addMeterNoEnergy(26706); QVERIFY(tm->findConfiguredThing(compteur) && tm->findConfiguredThing(r1)); + QVERIFY2(tm->findConfiguredThing(compteurSansEnergie), + "la classe meterNoEnergy doit être configurable — sinon la branche par défaut du " + "mock a parlé, et c'est elle qu'il faut lire"); + // Le contrat de la classe, vérifié plutôt que supposé : elle publie une puissance et ne + // totalise RIEN. Si un jour quelqu'un lui ajoutait le cumul, ce test-ci se tairait et le + // cas `meterWithoutEnergy` redeviendrait infabricable sans que rien ne le signale. + Thing *tSans = tm->findConfiguredThing(compteurSansEnergie); + QVERIFY2(tSans->thingClass().hasStateType("currentPower"), "un compteur publie une puissance"); + QVERIFY2(!tSans->thingClass().hasStateType("totalEnergyConsumed"), + "meterNoEnergy ne doit PAS totaliser — c'est toute sa raison d'être"); // Écrit par la FRONTIÈRE RPC, comme l'app le ferait — c'est le magasin que le handler // connaît. Un store créé à côté ne serait pas celui qu'il interroge : constaté ici même, @@ -7457,7 +7473,7 @@ void Simulation::testProgressMeasurabilityIsDeclaredAtConfigTime() }; aPoser << relais("sans-compteur", r1, QString()); aPoser << relais("compteur-ok", r2, compteur.toString()); - aPoser << relais("compteur-sans-energie", r3, r1.toString()); + aPoser << relais("compteur-sans-energie", r3, compteurSansEnergie.toString()); QCOMPARE(injectAndWait("NymeaEnergy.SetLoadConfig", QVariantMap({{"loadConfigs", aPoser}})).toMap() .value("params").toMap().value("energyError").toString(), @@ -7493,6 +7509,118 @@ void Simulation::testProgressMeasurabilityIsDeclaredAtConfigTime() #endif } +/*! + * \brief [Inventaire du mock] La branche THERMIQUE, écrite depuis toujours et jamais exécutée. + * + * \par Le manque + * `attachMeasurements()` teste `hasState("temperature")` sur le Thing désigné par + * `sensorThingId`. **Aucune classe du mock ne portait cet état** : la branche « sonde présente » + * n'avait jamais été parcourue, et rien n'exerçait la seule entrée du volet thermique. + * + * \par Les trois cas, et le troisième est celui qu'on n'aurait pas pensé à écrire + * Aucune sonde désignée, une sonde qui mesure, et une sonde DÉSIGNÉE QUI NE MESURE PAS — un + * Thing existant sans état `temperature`. Le troisième produit la même charge utile que le + * premier (`temperatureC` absent), alors que le geste de réparation n'est pas le même : dans un + * cas il faut désigner une sonde, dans l'autre en changer. + */ +void Simulation::testThermalProbeFeedsTelemetry() +{ +#ifndef ETM_ARBITRATOR + QSKIP("nécessite ETM_ARBITRATOR."); +#else + const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-thermo.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(); + + // Sans compteur racine, AUCUN cycle d'arbitrage ne tourne et `loads[]` reste vide : la + // garde d'assertion vacante ci-dessous l'a attrapé à la première exécution de ce test. + QUuid meterId = addMeter(26710); + m_experiencePlugin->energyManager()->setRootMeter(meterId); + Thing *meter = tm->findConfiguredThing(meterId); + QVERIFY(meter); + meter->setStateValue("connected", true); + + QUuid r1 = addPowerSwitch(1000, 26711); + QUuid r2 = addPowerSwitch(1000, 26712); + QUuid r3 = addPowerSwitch(1000, 26713); + QUuid sonde = addThermalProbe(26714); + QVERIFY2(tm->findConfiguredThing(sonde), "la classe thermalProbe doit être configurable"); + + Thing *tSonde = tm->findConfiguredThing(sonde); + QVERIFY2(tSonde->thingClass().hasStateType("temperature"), + "sans cet état, la branche testée reste inatteignable et le test serait VACANT"); + tSonde->setStateValue("temperature", 54.5); + + int rang = 0; + QVariantList aPoser; + auto charge = [&](const QString &id, const QUuid &relais, const QString &capteur) { + ++rang; + QVariantMap m{{"id", id}, {"label", id}, {"adapter", "relay-router"}, {"mode", "fixed"}, + {"priority", rang}, {"enabled", true}, {"domain", "ecs"}, + {"relays", QVariantList() << QVariantMap{{"thingId", relais.toString()}, + {"powerW", 1000}}}}; + if (!capteur.isEmpty()) m.insert("sensorThingId", capteur); + return m; + }; + aPoser << charge("sans-sonde", r1, QString()); + aPoser << charge("avec-sonde", r2, sonde.toString()); + // Une sonde DÉSIGNÉE qui ne mesure pas la température : un relais, ici. Le cas existe sur le + // terrain dès qu'on désigne le mauvais Thing, et il ne se distingue pas du premier. + aPoser << charge("sonde-muette", r3, r1.toString()); + QCOMPARE(injectAndWait("NymeaEnergy.SetLoadConfig", + QVariantMap({{"loadConfigs", aPoser}})).toMap() + .value("params").toMap().value("energyError").toString(), + QString("EnergyErrorNoError")); + + // Cycle DÉCLENCHÉ par l'horloge de simulation, jamais attendu de l'horloge murale. + const QDateTime t0 = utcDateTime(QDate(2026, 9, 5), QTime(13, 0, 0)); + meter->setStateValue("currentPower", -6000); // 6000 W exportés : de quoi servir + arb->simulationCallUpdate(t0); + QCoreApplication::processEvents(); + + QVariantList loads = injectAndWait("NymeaEnergy.GetLoadTelemetry").toMap() + .value("params").toMap().value("loads").toList(); + QHash parId; + for (const QVariant &v : loads) parId.insert(v.toMap().value("loadId").toString(), v.toMap()); + + // Garde contre l'assertion VACANTE — sans les trois charges, tout ce qui suit passerait + // sans rien vérifier. + QCOMPARE(parId.count(), 3); + + // 1. SONDE QUI MESURE — la branche qui n'avait jamais été exécutée. + QVERIFY2(parId.value("avec-sonde").contains("temperatureC"), + "une sonde désignée ET capable doit publier sa lecture"); + QCOMPARE(parId.value("avec-sonde").value("temperatureC").toDouble(), 54.5); + + // 2. AUCUNE SONDE — le champ est ABSENT, jamais zéro. Un 0 se lirait « il fait 0 °C ». + QVERIFY2(!parId.value("sans-sonde").contains("temperatureC"), + "sans sonde, le champ est absent — un zéro serait une mesure"); + + // 3. SONDE DÉSIGNÉE MAIS MUETTE — même charge utile que le cas 2, geste de réparation + // DIFFÉRENT. C'est un silence de la famille `progressUnmeasurableCause`, relevé ici et + // non corrigé : le corriger demande un catalogue de causes thermiques qui n'existe pas. + QVERIFY2(!parId.value("sonde-muette").contains("temperatureC"), + "une sonde incapable ne publie rien — mais rien ne dit POURQUOI"); + + // 4. La lecture SUIT la sonde : une valeur figée passerait les trois assertions ci-dessus. + tSonde->setStateValue("temperature", 61.25); + arb->simulationCallUpdate(t0.addSecs(60)); + QCoreApplication::processEvents(); + loads = injectAndWait("NymeaEnergy.GetLoadTelemetry").toMap() + .value("params").toMap().value("loads").toList(); + for (const QVariant &v : loads) + if (v.toMap().value("loadId").toString() == "avec-sonde") + QCOMPARE(v.toMap().value("temperatureC").toDouble(), 61.25); +#endif +} + /*! * \brief [Inventaire du mock] « Saine mais NON MESURÉE » n'est pas « en défaut ». * diff --git a/tests/auto/simulation/simulation.h b/tests/auto/simulation/simulation.h index cd687ca..1fbcce2 100644 --- a/tests/auto/simulation/simulation.h +++ b/tests/auto/simulation/simulation.h @@ -199,6 +199,7 @@ private slots: void testEcoFloorMissedSaysWhatWasLostAndWhy(); void testCommandDivergenceIsPublishedNotDiagnosed(); void testProgressMeasurabilityIsDeclaredAtConfigTime(); + void testThermalProbeFeedsTelemetry(); void testHealthyButUnmeasuredDiffersFromFaulty(); void testLifetimeCounterDropNeverRebasesTheProgress(); void testWorstPhaseBindsWhenPhasesAreUnbalanced(); diff --git a/tests/mocks/plugins/energymocks/integrationpluginenergymocks.cpp b/tests/mocks/plugins/energymocks/integrationpluginenergymocks.cpp index 29af85d..80f242c 100644 --- a/tests/mocks/plugins/energymocks/integrationpluginenergymocks.cpp +++ b/tests/mocks/plugins/energymocks/integrationpluginenergymocks.cpp @@ -487,7 +487,72 @@ void IntegrationPluginEnergyMocks::setupThing(ThingSetupInfo *info) qCDebug(dcEnergyMocks()) << "Setting up etmVariableLoad" << thing->name() << "finished successfully"; info->finish(Thing::ThingErrorNoError); return; + + } else if (thing->thingClassId() == meterNoEnergyThingClassId) { + // COMPTEUR SANS CUMUL D'ÉNERGIE — il publie une puissance, il ne totalise rien. + // + // Le cas était jusqu'ici fabriqué en désignant un `powerSwitch` comme compteur : un + // INTERRUPTEUR dans le rôle d'un instrument de mesure, et de surcroît la même Thing + // jouant deux rôles dans le même scénario. Le moteur ne teste que + // `hasStateType("totalEnergyConsumed")`, donc le détour marchait — mais il modélisait + // un montage que personne ne fait, et il empêchait de voir ce que le vrai cas produit. + // + // La classe ne déclare PAS `energymeter` : cette interface EXIGE `totalEnergyConsumed` + // et `totalEnergyProduced`. Promettre une interface qu'on ne tient pas ferait mentir le + // mock exactement là où il doit être littéral. `smartmeter` est la base sans état + // obligatoire, et c'est précisément ce qu'est ce matériel — mesuré sur l'ECS du banc + // le 2026-09-01. + EnergyMockController *controller = new EnergyMockController(thing, this); + ParamType paramType = thing->thingClass().paramTypes().findByName("port"); + quint16 port = thing->paramValue(paramType.id()).toUInt(); + if (!controller->listen(QHostAddress::Any, port)) { + qCWarning(dcEnergyMocks()) << "Failed to start mock controller on port" << controller->errorString(); + delete controller; + info->finish(Thing::ThingErrorThingInUse); + return; + } + connect(controller, &EnergyMockController::updateStateRequestReceived, thing, [=](const QUrlQuery &query){ + for (const QString &etat : {QStringLiteral("currentPower"), + QStringLiteral("voltagePhaseA"), + QStringLiteral("currentPhaseA")}) { + if (query.hasQueryItem(etat)) + thing->setStateValue(etat, QVariant(query.queryItemValue(etat)).toDouble()); + } + }); + m_controllers.insert(thing, controller); + info->finish(Thing::ThingErrorNoError); + return; + + } else if (thing->thingClassId() == thermalProbeThingClassId) { + // SONDE DE TEMPÉRATURE — la seule entrée du volet thermique, et rien ne l'exerçait. + // `EnergyArbitrator::attachMeasurements()` teste `hasState("temperature")` sur le Thing + // désigné par `sensorThingId` : aucune classe du mock ne portait cet état, donc la + // branche « sonde présente » était écrite et n'avait JAMAIS été exécutée. + EnergyMockController *controller = new EnergyMockController(thing, this); + ParamType paramType = thing->thingClass().paramTypes().findByName("port"); + quint16 port = thing->paramValue(paramType.id()).toUInt(); + if (!controller->listen(QHostAddress::Any, port)) { + qCWarning(dcEnergyMocks()) << "Failed to start mock controller on port" << controller->errorString(); + delete controller; + info->finish(Thing::ThingErrorThingInUse); + return; + } + connect(controller, &EnergyMockController::updateStateRequestReceived, thing, [=](const QUrlQuery &query){ + if (query.hasQueryItem("temperature")) + thing->setStateValue("temperature", QVariant(query.queryItemValue("temperature")).toDouble()); + }); + m_controllers.insert(thing, controller); + info->finish(Thing::ThingErrorNoError); + return; } + + // SORTIE PAR DÉFAUT, et elle manquait. Une classe sans branche ne tombait sur AUCUN + // `info->finish()` : le setup ne échouait pas, il ne se terminait jamais. Le symptôme est + // un Thing éternellement « en cours de configuration » et un test qui expire ailleurs, très + // loin de la cause. Une classe oubliée doit se dire, pas se taire. + qCWarning(dcEnergyMocks()) << "Aucune branche de setup pour la classe" + << thing->thingClass().name() << "— Thing non configurable."; + info->finish(Thing::ThingErrorSetupFailed); } void IntegrationPluginEnergyMocks::thingRemoved(Thing *thing) diff --git a/tests/mocks/plugins/energymocks/integrationpluginenergymocks.json b/tests/mocks/plugins/energymocks/integrationpluginenergymocks.json index 3c14974..8d4f428 100644 --- a/tests/mocks/plugins/energymocks/integrationpluginenergymocks.json +++ b/tests/mocks/plugins/energymocks/integrationpluginenergymocks.json @@ -1127,8 +1127,92 @@ "writable": true } ] + }, + { + "name": "meterNoEnergy", + "displayName": "Mocked Meter (puissance SEULE, sans cumul d'énergie)", + "id": "50dacc7b-7142-4716-8f59-97d6ee81f39c", + "createMethods": [ + "user" + ], + "interfaces": [ + "smartmeter" + ], + "paramTypes": [ + { + "id": "eec1f5a2-5c59-4ce9-a5ae-5dc7cad70772", + "name": "port", + "displayName": "Port", + "type": "uint", + "defaultValue": 26671 + } + ], + "stateTypes": [ + { + "id": "354ab572-b272-4ff0-b99c-51791543a3df", + "name": "currentPower", + "displayName": "Current power", + "displayNameAction": "Set current power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "writable": true + }, + { + "id": "7a3eabcc-d124-40b2-b801-f51767400d4c", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "displayNameAction": "Set voltage phase A", + "type": "double", + "unit": "Volt", + "defaultValue": 230, + "writable": true + }, + { + "id": "4fea5d43-5d92-49f5-b045-46f471d76b00", + "name": "currentPhaseA", + "displayName": "Current phase A", + "displayNameAction": "Set current phase A", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "writable": true + } + ] + }, + { + "name": "thermalProbe", + "displayName": "Mocked Thermal Probe (sonde de température)", + "id": "37e61ea3-e4b2-4d3e-bc82-66e350c29f66", + "createMethods": [ + "user" + ], + "interfaces": [ + "temperaturesensor" + ], + "paramTypes": [ + { + "id": "b898397c-c291-47f9-82ec-662250b932e1", + "name": "port", + "displayName": "Port", + "type": "uint", + "defaultValue": 26681 + } + ], + "stateTypes": [ + { + "id": "098b3a75-59dd-4cd5-8595-9cd66fedfd84", + "name": "temperature", + "displayName": "Temperature", + "displayNameAction": "Set temperature", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 20.0, + "writable": true + } + ] } ] } ] -} +} \ No newline at end of file