etm-powersync-energy-plugin.../tests/mocks/plugins/energymocks/integrationpluginenergymocks.cpp
Patrick Schurig c88ba6ad45 [upstream-fix] rootmeter: la tension est locale à chaque phase
`voltage` était déclarée une seule fois hors des trois blocs et mutée par
l'un d'eux : une phase sans tension déclarée était calculée avec celle d'une
autre.

Et le défaut n'était pas celui qu'on croyait — le mesurer l'a montré.
L'interface energymeter rend currentPhaseA et voltagePhaseA OBLIGATOIRES, B
et C optionnels : la branche « puissance ÷ tension » de A est morte par
construction. La fuite n'allait donc pas de A vers B, comme la note d'origine
l'affirmait, mais de B vers C.

Démontré : B à 200 V, C à 4 000 W sans tension déclarée → 5,0 A de marge au
lieu de 7,6. Deux conditions, dont aucune n'était évidente avant d'écrire le
test : le masque doit couvrir B ET C — interroger C seule n'exécute pas le
bloc de B — et C doit être la phase la plus contrainte, sinon le qMin retient
B et masque l'écart. Mes deux premières versions du test passaient pour ces
deux raisons.

Le mock ne savait pas produire le cas : il a fallu meterPowerOnly, avec les
puissances et la tension de B mais sans les courants de B/C ni la tension de
C. Troisième fois qu'ouvrir le mock révèle un défaut plutôt que de seulement
débloquer un test.

draw.bindingPhase dit LAQUELLE des phases borne. perPhaseBound disait qu'une
phase borne, jamais laquelle : l'écran ne pouvait dire que « bridé » sans
dire par quoi. DRAW_CAP la porte aussi mais n'est émis que si une charge est
refusée — un cycle qui borde sans rien refuser laisserait l'écran muet.

Et dans AGENTS.md : une conclusion juste posée sur un raisonnement faux
survit jusqu'à ce qu'on bâtisse dessus, parce que la conclusion PROTÈGE le
raisonnement de l'examen. Un résultat faux se voit ; un résultat juste ferme
la question. Parade : quand une décision est écrite avec sa raison, la raison
mérite son propre contrôle — c'est elle que le lot suivant utilisera.

Suite : simulation 65/65, loadmodel 22/22, charging 48/48 identique à la
référence, spotmarket 32/32, doxygen 0 avertissement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-09-04 06:02:17 +02:00

698 lines
35 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::setupThing(ThingSetupInfo *info)
{
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 &paramTypeId, 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;
}
}
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);
}
}