`meterNoEnergy` remplace le `powerSwitch` détourné en instrument de mesure. Le
détour marchait — le moteur ne teste que `hasStateType("totalEnergyConsumed")` —
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 d'une charge et compteur d'une autre.
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.
`thermalProbe` ouvre le volet thermique. La branche « sonde présente » de
attachMeasurements() était écrite depuis toujours et n'avait JAMAIS été parcourue :
aucune classe du mock ne portait l'état `temperature`. C'était la seule entrée du
volet thermique, et rien ne l'exerçait.
CE QUE L'OUVERTURE A RÉVÉLÉ, comme les trois précédentes. `setupThing()` du mock
n'avait AUCUNE sortie par défaut : une classe sans branche ne rencontrait jamais
d'`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. 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. Une classe oubliée se dit maintenant, en qCWarning + SetupFailed.
UNE GARDE A MORDU sur son auteur le jour même. 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 dans INVENTAIRE_MOCK.md, appliquée à elle-même.
RELEVÉ, NON CORRIGÉ : une sonde DÉSIGNÉE mais incapable produit la même charge
utile qu'aucune sonde du tout (`temperatureC` absent), alors que le geste de
réparation diffère — désigner une sonde, ou en changer. Silence de la famille
`progressUnmeasurableCause` ; le fermer demande un catalogue de causes thermiques
qui n'existe pas encore.
Suites : simulation 67/67 (+1), loadmodel 23/23, charging 48/48, spotmarket 32/32.
799 lines
40 KiB
C++
799 lines
40 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea-energy-plugin-nymea.
|
|
*
|
|
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "integrationpluginenergymocks.h"
|
|
|
|
#include "plugininfo.h"
|
|
#include "mockcontroller.h"
|
|
|
|
#include <QTimer>
|
|
|
|
#include <hardware/electricity.h>
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
}
|
|
|