totalEnergyConsumed n'était écrit par personne : il valait sa valeur par défaut, indéfiniment. Un compteur figé est un compteur qui n'existe pas pour tout ce qui l'intègre — deliveredWh restait à 0, remainingWh à 4 000, et l'étalement adaptatif n'était jamais exercé. Le comportement se confondait avec unmeasurable pour une raison entièrement différente et bénigne. Quatrième occurrence d'un mock plus homogène que la réalité, après les six états cohérents du rootmeter, les classes portant toutes une puissance, et la triphasée équilibrée. En export il ne recule pas — il ne bouge pas : un compteur de consommation est monotone, et le faire reculer produirait un cas que le terrain ne produit pas, celui-là même que la garde de sémantique traite comme une anomalie. Le mock fabriquerait le défaut que le moteur est censé détecter. LM-1014-b appliquée au mock, question posée et répondue : le cumul est déclaré `cached`, donc il continue au redémarrage. Repartir de zéro ferait reculer un cumul monotone. Et purchasedWh gagne un second usage, sous `measured` cette fois. Mesuré sur le banc : il accumule pendant que deliveredWh peut rester plat, et cette divergence est un contrôle que l'écran n'aurait pas su fabriquer — de l'acheté qui monte avec du livré plat signifie soit que la charge ne consomme pas ce qu'on paie, soit que le compteur ne compte pas. C'est écrit au contrat parce que le jour où les deux coïncideront, quelqu'un jugera le champ redondant : c'est précisément quand ils divergent que la paire devient informative, et un champ qui ne sert que dans l'anomalie n'a pas l'air utile le reste du temps. La borne basse du test de cumul est large, et délibérément : il ne contrôle ni la cadence du minuteur ni l'alignement du premier tic, qui n'intègre rien. Une borne serrée en ferait un test instable — qu'on relancerait jusqu'à ce qu'il passe. Suite : simulation 66/66, 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
734 lines
36 KiB
C++
734 lines
36 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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
}
|
|
|