// 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 . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationpluginenergymocks.h" #include "plugininfo.h" #include "mockcontroller.h" #include #include IntegrationPluginEnergyMocks::IntegrationPluginEnergyMocks(QObject *parent) : IntegrationPlugin(parent) { } void IntegrationPluginEnergyMocks::integrerEnergie() { const QDateTime maintenant = QDateTime::currentDateTime(); for (Thing *thing : myThings()) { if (!thing->thingClass().hasStateType("totalEnergyConsumed") || !thing->thingClass().hasStateType("currentPower")) continue; const QDateTime avant = m_derniereIntegration.value(thing); m_derniereIntegration.insert(thing, maintenant); // Première passe : aucune durée derrière elle. Intégrer y fabriquerait de l'énergie. if (!avant.isValid()) continue; const double heures = avant.msecsTo(maintenant) / 3600000.0; const double puissanceW = thing->stateValue("currentPower").toDouble(); // Un compteur de CONSOMMATION n'intègre que ce qui est consommé : en export, il ne // recule pas — il ne bouge pas. Le faire reculer produirait un cas que le terrain ne // produit pas, et que la garde de sémantique traiterait comme une anomalie. if (puissanceW <= 0 || heures <= 0) continue; const double avantKWh = thing->stateValue("totalEnergyConsumed").toDouble(); thing->setStateValue("totalEnergyConsumed", avantKWh + puissanceW * heures / 1000.0); } } void IntegrationPluginEnergyMocks::setupThing(ThingSetupInfo *info) { // Intégrateur d'énergie, armé une fois. Une seconde : assez fin pour qu'un cycle // d'arbitrage (à la minute) voie le compteur BOUGER, ce qui est tout l'objet. if (!m_integrateur) { m_integrateur = new QTimer(this); m_integrateur->setInterval(1000); connect(m_integrateur, &QTimer::timeout, this, &IntegrationPluginEnergyMocks::integrerEnergie); m_integrateur->start(); } Thing *thing = info->thing(); qCDebug(dcEnergyMocks()) << "Setting up" << thing << thing->params(); if (thing->thingClassId() == chargerThingClassId) { 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("connected")) thing->setStateValue("connected", QVariant(query.queryItemValue("connected")).toBool()); if (query.hasQueryItem("power")) thing->setStateValue("power", QVariant(query.queryItemValue("power")).toBool()); if (query.hasQueryItem("pluggedIn")) thing->setStateValue("pluggedIn", QVariant(query.queryItemValue("pluggedIn")).toBool()); if (query.hasQueryItem("usedPhases")) { thing->setStateValue("usedPhases", query.queryItemValue("usedPhases")); thing->setStateValue("phaseCount", Electricity::getPhaseCount(Electricity::convertPhasesFromString(query.queryItemValue("usedPhases")))); } if (query.hasQueryItem("maxChargingCurrent")) thing->setStateValue("maxChargingCurrent", QVariant(query.queryItemValue("maxChargingCurrent")).toInt()); if (query.hasQueryItem("maxChargingCurrentMaxValue")) thing->setStateMaxValue("maxChargingCurrent", QVariant(query.queryItemValue("maxChargingCurrentMaxValue")).toInt()); updateChargerMeter(thing); qCDebug(dcEnergyMocks()) << "--> States" << thing->name(); foreach (const State &state, thing->states()) qCDebug(dcEnergyMocks()) << " -" << thing->thingClass().stateTypes().findById(state.stateTypeId()).displayName() << ":" << state.value(); }); qCDebug(dcEnergyMocks()) << "Setting up charger" << thing->name() << "finished successfully"; m_controllers.insert(thing, controller); info->finish(Thing::ThingErrorNoError); thing->setStateValue("usedPhases", thing->paramValue("phases").toString()); thing->setStateValue("phaseCount", Electricity::getPhaseCount(Electricity::convertPhasesFromString(thing->stateValue("usedPhases").toString()))); thing->setStateMaxValue("maxChargingCurrent", QVariant(thing->paramValue("maxChargingCurrentUpperLimit")).toInt()); updateChargerMeter(thing); return; } else if (thing->thingClassId() == chargerPhaseSwitchingThingClassId) { 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("connected")) thing->setStateValue("connected", QVariant(query.queryItemValue("connected")).toBool()); if (query.hasQueryItem("power")) thing->setStateValue("power", QVariant(query.queryItemValue("power")).toBool()); if (query.hasQueryItem("pluggedIn")) thing->setStateValue("pluggedIn", QVariant(query.queryItemValue("pluggedIn")).toBool()); if (query.hasQueryItem("usedPhases")) { thing->setStateValue("usedPhases", query.queryItemValue("usedPhases")); thing->setStateValue("phaseCount", Electricity::getPhaseCount(Electricity::convertPhasesFromString(query.queryItemValue("usedPhases")))); } if (query.hasQueryItem("maxChargingCurrent")) thing->setStateValue("maxChargingCurrent", QVariant(query.queryItemValue("maxChargingCurrent")).toInt()); if (query.hasQueryItem("maxChargingCurrentMaxValue")) thing->setStateMaxValue("maxChargingCurrent", QVariant(query.queryItemValue("maxChargingCurrentMaxValue")).toInt()); if (query.hasQueryItem("desiredPhaseCount")) thing->setStateValue("desiredPhaseCount", QVariant(query.queryItemValue("desiredPhaseCount")).toUInt()); updateChargerMeter(thing); qCDebug(dcEnergyMocks()) << "--> States" << thing->name(); foreach (const State &state, thing->states()) qCDebug(dcEnergyMocks()) << " -" << thing->thingClass().stateTypes().findById(state.stateTypeId()).displayName() << ":" << state.value(); }); qCDebug(dcEnergyMocks()) << "Setting up charger" << thing->name() << "finished successfully"; m_controllers.insert(thing, controller); info->finish(Thing::ThingErrorNoError); thing->setStateValue("usedPhases", thing->paramValue("phases").toString()); thing->setStateValue("phaseCount", Electricity::getPhaseCount(Electricity::convertPhasesFromString(thing->stateValue("usedPhases").toString()))); thing->setStateMaxValue("maxChargingCurrent", QVariant(thing->paramValue("maxChargingCurrentUpperLimit")).toInt()); updateChargerMeter(thing); return; } else if (thing->thingClassId() == simpleChargerThingClassId) { 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("connected")) thing->setStateValue("connected", QVariant(query.queryItemValue("connected")).toBool()); if (query.hasQueryItem("power")) thing->setStateValue("power", QVariant(query.queryItemValue("power")).toBool()); if (query.hasQueryItem("pluggedIn")) thing->setStateValue("pluggedIn", QVariant(query.queryItemValue("pluggedIn")).toBool()); if (query.hasQueryItem("phaseCount")) thing->setStateValue("phaseCount", QVariant(query.queryItemValue("phaseCount")).toInt()); if (query.hasQueryItem("maxChargingCurrent")) thing->setStateValue("maxChargingCurrent", QVariant(query.queryItemValue("maxChargingCurrent")).toInt()); if (query.hasQueryItem("maxChargingCurrentMaxValue")) thing->setStateMaxValue("maxChargingCurrent", QVariant(query.queryItemValue("maxChargingCurrentMaxValue")).toInt()); qCDebug(dcEnergyMocks()) << "--> States" << thing->name(); foreach (const State &state, thing->states()) qCDebug(dcEnergyMocks()) << " -" << thing->thingClass().stateTypes().findById(state.stateTypeId()).displayName() << ":" << state.value(); }); m_controllers.insert(thing, controller); qCDebug(dcEnergyMocks()) << "Setting up simple charger" << thing->name() << "finished successfully"; info->finish(Thing::ThingErrorNoError); return; } else if (thing->thingClassId() == meterThingClassId || thing->thingClassId() == meterPowerOnlyThingClassId) { // `meterPowerOnly` partage tout le comportement du compteur : seule sa ThingClass diffère // (pas de `currentPhaseB/C`, pas de `voltagePhaseC`). C'est l'ABSENCE d'états qui est // l'objet de la classe, pas un comportement particulier. 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("connected")) thing->setStateValue("connected", QVariant(query.queryItemValue("connected")).toBool()); if (query.hasQueryItem("originalPower")) { // Split the original power on the 3 phases and add the current power (depending on the phases) from the charger // double originalPhasePower = query.queryItemValue("originalPower").toInt() / 3.0; // double phaseA = originalPhasePower; // double phaseB = originalPhasePower; // double phaseC = originalPhasePower; // // Get charger phases, set each phase and then the sum // foreach (Thing *chargerThing, myThings().filterByThingClassId(chargerThingClassId)) { // Electricity::Phases phases = Electricity::convertPhasesFromString(thing->paramValue("phases").toString()); // // // } } if (query.hasQueryItem("currentPowerPhaseA") && query.hasQueryItem("currentPowerPhaseB") && query.hasQueryItem("currentPowerPhaseC")) { // We directly set the current power on the 3 phases, no need to perform calculations (for tests to emulate a certain situation) int currentPowerPhaseA = query.queryItemValue("currentPowerPhaseA").toInt(); int currentPowerPhaseB = query.queryItemValue("currentPowerPhaseB").toInt(); int currentPowerPhaseC = query.queryItemValue("currentPowerPhaseC").toInt(); // TENSION ET COURANT PAR PHASE, réglables INDÉPENDAMMENT des puissances. // // Sans cela, le mock posait `currentPhaseX = currentPowerPhaseX / 230` : les deux // familles d'états étaient COHÉRENTES PAR CONSTRUCTION, et aucun scénario ne // pouvait produire une tension réelle ≠ 230 V, un déséquilibre entre phases, ni // un compteur qui ne publierait qu'une des deux familles. C'est ce qui a rendu le // défaut d'asymétrie de `calculateAllowanceAmpere()` invisible à la suite // pendant des mois (docs/INVENTAIRE_MOCK.md, manque n°2). // // Les paramètres sont OPTIONNELS : sans eux, le comportement dérivé d'origine est // conservé, et aucun scénario existant ne bouge. auto tension = [&](const char *nom) { return query.hasQueryItem(nom) ? query.queryItemValue(nom).toDouble() : 230.0; }; const double uA = tension("voltagePhaseA"); const double uB = tension("voltagePhaseB"); const double uC = tension("voltagePhaseC"); // N'écrire que ce que la ThingClass PORTE : `meterPowerOnly` n'a ni // `voltagePhaseC` ni les courants de B et C, et c'est tout son objet. auto poser = [&](const char *n, double v) { if (thing->thingClass().hasStateType(n)) thing->setStateValue(n, v); }; poser("voltagePhaseA", uA); poser("voltagePhaseB", uB); poser("voltagePhaseC", uC); // Le COURANT peut être imposé directement — un compteur réel le mesure, il ne le // dérive pas de la puissance. poser("currentPhaseA", query.hasQueryItem("currentPhaseA") ? query.queryItemValue("currentPhaseA").toDouble() : currentPowerPhaseA / uA); poser("currentPhaseB", query.hasQueryItem("currentPhaseB") ? query.queryItemValue("currentPhaseB").toDouble() : currentPowerPhaseB / uB); poser("currentPhaseC", query.hasQueryItem("currentPhaseC") ? query.queryItemValue("currentPhaseC").toDouble() : currentPowerPhaseC / uC); thing->setStateValue("currentPowerPhaseA", currentPowerPhaseA); thing->setStateValue("currentPowerPhaseB", currentPowerPhaseB); thing->setStateValue("currentPowerPhaseC", currentPowerPhaseC); // Note: set the current power at the end since that triggers caclulations.... thing->setStateValue("currentPower", currentPowerPhaseA + currentPowerPhaseB + currentPowerPhaseC); } qCDebug(dcEnergyMocks()) << "--> States" << thing->name(); foreach (const State &state, thing->states()) qCDebug(dcEnergyMocks()) << " -" << thing->thingClass().stateTypes().findById(state.stateTypeId()).displayName() << ":" << state.value(); }); m_controllers.insert(thing, controller); qCDebug(dcEnergyMocks()) << "Setting up meter" << thing->name() << "finished successfully"; info->finish(Thing::ThingErrorNoError); return; } else if (thing->thingClassId() == carThingClassId) { 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("batteryLevel")) thing->setStateValue("batteryLevel", QVariant(query.queryItemValue("batteryLevel")).toInt()); if (query.hasQueryItem("capacity")) thing->setStateValue("capacity", QVariant(query.queryItemValue("capacity")).toInt()); if (query.hasQueryItem("minChargingCurrent")) thing->setStateValue("minChargingCurrent", QVariant(query.queryItemValue("minChargingCurrent")).toInt()); if (query.hasQueryItem("phaseCount")) { thing->setSettingValue(carSettingsPhaseCountParamTypeId, QVariant(query.queryItemValue("phaseCount")).toInt()); thing->setStateValue("phaseCount", QVariant(query.queryItemValue("phaseCount")).toInt()); } }); // Set the min charging current state if the settings value changed connect(thing, &Thing::settingChanged, controller, [thing](const ParamTypeId ¶mTypeId, const QVariant &value){ if (paramTypeId == carSettingsCapacityParamTypeId) { qCDebug(dcEnergyMocks()) << "Car capacity settings changed" << value << "kWh"; thing->setStateValue(carCapacityStateTypeId, value); } else if (paramTypeId == carSettingsMinChargingCurrentParamTypeId) { qCDebug(dcEnergyMocks()) << "Car minimum charging current settings changed" << value.toUInt() << "A"; thing->setStateValue(carMinChargingCurrentStateTypeId, value); } else if (paramTypeId == carSettingsPhaseCountParamTypeId) { qCDebug(dcEnergyMocks()) << "Car phase count settings changed" << value.toUInt(); thing->setStateValue(carPhaseCountStateTypeId, value); } qCDebug(dcEnergyMocks()) << "--> States" << thing->name(); foreach (const State &state, thing->states()) qCDebug(dcEnergyMocks()) << " -" << thing->thingClass().stateTypes().findById(state.stateTypeId()).displayName() << ":" << state.value(); }); qCDebug(dcEnergyMocks()) << "Setting car" << thing->name() << "finished successfully"; m_controllers.insert(thing, controller); info->finish(Thing::ThingErrorNoError); thing->setStateValue(carMinChargingCurrentStateTypeId, thing->setting(carSettingsMinChargingCurrentParamTypeId)); return; } else if (thing->thingClassId() == notificationThingClassId) { 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){ Q_UNUSED(query) }); m_controllers.insert(thing, controller); qCDebug(dcEnergyMocks()) << "Setting up notification" << thing->name() << "finished successfully"; info->finish(Thing::ThingErrorNoError); return; } else if (thing->thingClassId() == energyStorageThingClassId) { 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)) { qWarning(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("batteryLevel")) { thing->setStateValue("batteryLevel", QVariant(query.queryItemValue("batteryLevel")).toInt()); thing->setStateValue("batteryCritical", thing->stateValue("batteryLevel").toInt() < 10); } if (query.hasQueryItem("currentPower")) thing->setStateValue("currentPower", QVariant(query.queryItemValue("currentPower")).toInt()); }); qCDebug(dcEnergyMocks()) << "Setting battery storage" << thing->name() << "finished successfully"; m_controllers.insert(thing, controller); info->finish(Thing::ThingErrorNoError); thing->setStateValue("capacity", thing->paramValue(energyStorageThingCapacityParamTypeId)); return; } else if (thing->thingClassId() == dryRelayThingClassId) { // RELAIS SEC — il commute, il ne mesure pas. C'est toute sa raison d'être : le mock ne // savait produire aucune charge pilotable SANS puissance, si bien qu'une charge « non // mesurée mais saine » était impossible à fabriquer, alors qu'un relais sec est banal // sur le terrain. Sans lui, aucun test ne peut distinguer « non mesurée » de « en // défaut » (docs/INVENTAIRE_MOCK.md). info->finish(Thing::ThingErrorNoError); return; } else if (thing->thingClassId() == powerSwitchThingClassId) { 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){ // Permet au test d'imposer power / currentPower directement (ex. émuler une // mesure dérivée, ou un thermostat coupé : power=true mais currentPower=0). if (query.hasQueryItem("power")) thing->setStateValue("power", QVariant(query.queryItemValue("power")).toBool()); if (query.hasQueryItem("currentPower")) thing->setStateValue("currentPower", QVariant(query.queryItemValue("currentPower")).toDouble()); }); m_controllers.insert(thing, controller); qCDebug(dcEnergyMocks()) << "Setting up power switch" << thing->name() << "finished successfully"; info->finish(Thing::ThingErrorNoError); return; } else if (thing->thingClassId() == etmVariableLoadThingClassId) { // Mock etmvariableload : un controller (nécessaire pour logActionExecuted dans // executeAction) + injection éventuelle de currentPowerW. Le test impose plutôt // currentPowerW directement via setStateValue (mesure simulée de la charge). 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("currentPowerW")) thing->setStateValue("currentPowerW", QVariant(query.queryItemValue("currentPowerW")).toDouble()); }); m_controllers.insert(thing, controller); 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) { qCDebug(dcEnergyMocks()) << "Removing" << thing; if (m_controllers.contains(thing)) { delete m_controllers.take(thing); } } void IntegrationPluginEnergyMocks::executeAction(ThingActionInfo *info) { Thing *thing = info->thing(); ThingClass thingClass = info->thing()->thingClass(); Action action = info->action(); ActionType actionType = thingClass.actionTypes().findById(action.actionTypeId()); qCDebug(dcEnergyMocks()) << "Executing action" << actionType.displayName() << "on" << thing->name() << action.params(); // Toutes les classes n'ont pas de contrôleur HTTP : le relais SEC n'en a pas besoin, // n'ayant rien à mesurer ni à piloter à distance. Déréférencer sans vérifier plantait — // le mock supposait que chaque Thing en possédait un. if (EnergyMockController *c = m_controllers.value(thing)) c->logActionExecuted(actionType, action); // Update states if (thing->thingClassId() == dryRelayThingClassId) { if (actionType.name() == "power") { // Aucune écriture de `currentPower` : la classe ne porte pas l'état. C'est // l'absence qui est l'objet du mécanisme, pas une valeur à zéro. thing->setStateValue("power", action.paramValue(actionType.paramTypes().findByName("power").id()).toBool()); } info->finish(Thing::ThingErrorNoError); return; } if (thing->thingClassId() == chargerThingClassId) { // Note do this before every action execution since it might hase changed directly within the simulation Electricity::Phases usedPhases = Electricity::convertPhasesFromString(thing->stateValue("usedPhases").toString()); uint phaseCount = Electricity::getPhaseCount(usedPhases); thing->setStateValue("phaseCount", phaseCount); if (actionType.name() == "maxChargingCurrent") { uint maxChargingCurrent = action.paramValue(actionType.paramTypes().findByName("maxChargingCurrent").id()).toUInt(); thing->setStateValue("maxChargingCurrent", maxChargingCurrent); updateChargerMeter(thing); } if (actionType.name() == "power") { bool power = action.paramValue(actionType.paramTypes().findByName("power").id()).toBool(); qCDebug(dcEnergyMocks()) << "Setting charger power to" << power; thing->setStateValue("power", power); updateChargerMeter(thing); } if (actionType.name() == "update") { updateChargerMeter(thing); } } if (thing->thingClassId() == chargerPhaseSwitchingThingClassId) { // Note do this before every action execution since it might has changed directly within the simulation Electricity::Phases usedPhases = Electricity::convertPhasesFromString(thing->stateValue("usedPhases").toString()); uint phaseCount = Electricity::getPhaseCount(usedPhases); thing->setStateValue("phaseCount", phaseCount); if (actionType.name() == "maxChargingCurrent") { uint maxChargingCurrent = action.paramValue(actionType.paramTypes().findByName("maxChargingCurrent").id()).toUInt(); thing->setStateValue("maxChargingCurrent", maxChargingCurrent); updateChargerMeter(thing); } if (actionType.name() == "connected") { bool connected = action.paramValue(actionType.paramTypes().findByName("connected").id()).toBool(); thing->setStateValue("connected", connected); updateChargerMeter(thing); } if (actionType.name() == "pluggedIn") { bool pluggedIn = action.paramValue(actionType.paramTypes().findByName("pluggedIn").id()).toBool(); thing->setStateValue("pluggedIn", pluggedIn); updateChargerMeter(thing); } if (actionType.name() == "desiredPhaseCount") { uint desiredPhaseCount = action.paramValue(actionType.paramTypes().findByName("desiredPhaseCount").id()).toUInt(); thing->setStateValue("desiredPhaseCount", desiredPhaseCount); updateChargerMeter(thing); } if (actionType.name() == "power") { bool power = action.paramValue(actionType.paramTypes().findByName("power").id()).toBool(); qCDebug(dcEnergyMocks()) << "Setting charger power to" << power; thing->setStateValue("power", power); updateChargerMeter(thing); } if (actionType.name() == "update") { updateChargerMeter(thing); } } if (thing->thingClassId() == simpleChargerThingClassId) { if (actionType.name() == "maxChargingCurrent") { uint maxChargingCurrent = action.paramValue(actionType.paramTypes().findByName("maxChargingCurrent").id()).toUInt(); thing->setStateValue("maxChargingCurrent", action.paramValue(actionType.paramTypes().findByName("maxChargingCurrent").id()).toUInt()); qCDebug(dcEnergyMocks()) << "Setting max charging current to" << maxChargingCurrent << "A"; } if (actionType.name() == "power") { bool power = action.paramValue(actionType.paramTypes().findByName("power").id()).toBool(); qCDebug(dcEnergyMocks()) << "Setting charger power to" << power; thing->setStateValue("power", power); } } if (thing->thingClassId() == carThingClassId) { if (action.actionTypeId() == carBatteryLevelActionTypeId) { thing->setStateValue(carBatteryLevelStateTypeId, action.paramValue(carBatteryLevelActionBatteryLevelParamTypeId)); thing->setStateValue(carBatteryCriticalStateTypeId, action.paramValue(carBatteryLevelActionBatteryLevelParamTypeId).toInt() < 10); info->finish(Thing::ThingErrorNoError); return; } else { Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); } } if (thing->thingClassId() == powerSwitchThingClassId) { if (actionType.name() == "power") { bool power = action.paramValue(actionType.paramTypes().findByName("power").id()).toBool(); double nominal = thing->paramValue(thing->thingClass().paramTypes().findByName("nominalPower").id()).toDouble(); // Relais ON → consomme sa puissance nominale ; OFF → 0 W. Le test peut écraser // currentPower via /setstates (ex. émuler une mesure dérivée). thing->setStateValue("power", power); thing->setStateValue("currentPower", power ? nominal : 0.0); qCDebug(dcEnergyMocks()) << "Mock power switch" << thing->name() << "power" << power << "currentPower" << thing->stateValue("currentPower"); } } if (thing->thingClassId() == etmVariableLoadThingClassId) { if (actionType.name() == "powerSetpoint") { // L'EtmVariableLoadAdapter écrit la consigne ; le mock l'enregistre dans le state. // currentPowerW (puissance réellement appliquée) reste piloté par le test — // c'est le thing réel qui choisirait la combinaison matérielle (contrat §3). double setpoint = action.paramValue(actionType.paramTypes().findByName("powerSetpoint").id()).toDouble(); thing->setStateValue("powerSetpoint", setpoint); qCDebug(dcEnergyMocks()) << "Mock etmVariableLoad" << thing->name() << "powerSetpoint" << setpoint; } } info->finish(Thing::ThingErrorNoError); } void IntegrationPluginEnergyMocks::updateChargerMeter(Thing *thing) { Electricity::Phases connectedPhases = Electricity::convertPhasesFromString(thing->paramValue("phases").toString()); bool canSwitchPhases = thing->hasState("desiredPhaseCount"); Electricity::Phases usedPhases = Electricity::PhaseNone; if (canSwitchPhases) { uint desiredPhaseCount = thing->stateValue("desiredPhaseCount").toUInt(); if (desiredPhaseCount == 1) { usedPhases = Electricity::PhaseA; } else { usedPhases = Electricity::PhaseAll; } thing->setStateValue("usedPhases", Electricity::convertPhasesToString(usedPhases)); } else { usedPhases = Electricity::convertPhasesFromString(thing->stateValue("usedPhases").toString()); } uint phaseCount = Electricity::getPhaseCount(usedPhases); thing->setStateValue("phaseCount", phaseCount); if (connectedPhases.testFlag(Electricity::PhaseA)) { thing->setStateValue("voltagePhaseA", 230); } else { thing->setStateValue("voltagePhaseA", 0); } if (connectedPhases.testFlag(Electricity::PhaseB)) { thing->setStateValue("voltagePhaseB", 230); } else { thing->setStateValue("voltagePhaseB", 0); } if (connectedPhases.testFlag(Electricity::PhaseC)) { thing->setStateValue("voltagePhaseC", 230); } else { thing->setStateValue("voltagePhaseC", 0); } double maxChargingCurrent = thing->stateValue("maxChargingCurrent").toDouble(); if (thing->stateValue("power").toBool() && thing->stateValue("connected").toBool() && thing->stateValue("pluggedIn").toBool()) { double currentPower = maxChargingCurrent * 230 * phaseCount; double phasePower = currentPower / phaseCount; double phaseAmpere = maxChargingCurrent; if (usedPhases.testFlag(Electricity::PhaseA)) { thing->setStateValue("currentPhaseA", phaseAmpere); thing->setStateValue("currentPowerPhaseA", phasePower); } else { thing->setStateValue("currentPhaseA", 0); thing->setStateValue("currentPowerPhaseA", 0); } if (usedPhases.testFlag(Electricity::PhaseB)) { thing->setStateValue("currentPhaseB", phaseAmpere); thing->setStateValue("currentPowerPhaseB", phasePower); } else { thing->setStateValue("currentPhaseB", 0); thing->setStateValue("currentPowerPhaseB", 0); } if (usedPhases.testFlag(Electricity::PhaseC)) { thing->setStateValue("currentPhaseC", phaseAmpere); thing->setStateValue("currentPowerPhaseC", phasePower); } else { thing->setStateValue("currentPhaseC", 0); thing->setStateValue("currentPowerPhaseC", 0); } thing->setStateValue("currentPower", currentPower); thing->setStateValue("charging", true); } else { thing->setStateValue("currentPhaseA", 0); thing->setStateValue("currentPowerPhaseA", 0); thing->setStateValue("currentPhaseB", 0); thing->setStateValue("currentPowerPhaseB", 0); thing->setStateValue("currentPhaseC", 0); thing->setStateValue("currentPowerPhaseC", 0); thing->setStateValue("currentPower", 0); thing->setStateValue("charging", false); } }