`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
144 lines
7.1 KiB
C++
144 lines
7.1 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/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#ifndef ENERGYTESTBASE_H
|
|
#define ENERGYTESTBASE_H
|
|
|
|
#include <QObject>
|
|
#include <QtTest>
|
|
#include <QDateTime>
|
|
#include <QTcpSocket>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkRequest>
|
|
#include <QNetworkReply>
|
|
#include <QLoggingCategory>
|
|
|
|
#include <nymeatestbase.h>
|
|
|
|
#include <nymeacore.h>
|
|
#include <nymeasettings.h>
|
|
#include <servers/mocktcpserver.h>
|
|
|
|
#include <energymanager.h>
|
|
|
|
static QUuid mockMeterThingClassId = QUuid("2721a051-6e12-471a-baba-21d87c4cebc9");
|
|
//! \brief Compteur publiant les PUISSANCES par phase mais pas tous les COURANTS ni toutes les
|
|
//! TENSIONS. `energymeter` exige `currentPhaseA` et `voltagePhaseA` ; B et C sont OPTIONNELS —
|
|
//! c'est cette classe qui rend atteignable la branche « puissance ÷ tension » de
|
|
//! `calculateAllowanceAmpere()`, et donc la fuite de tension d'une phase vers la suivante.
|
|
static QUuid mockMeterPowerOnlyThingClassId = QUuid("3ac91e57-08b4-4f26-9d31-7e5a2c60b4f8");
|
|
static QUuid mockChargerThingClassId = QUuid("5a3ae99f-c7da-46df-9104-f477be4606b7");
|
|
static QUuid mockChargerWithPhaseSwitchingThingClassId = QUuid("9208d9f0-280c-469d-a145-106f3277470c");
|
|
static QUuid mockSimpleChargerThingClassId = QUuid("29bcf255-b654-4764-be92-399bc26fe7c3");
|
|
static QUuid mockCarThingClassId = QUuid("4513f801-836e-40a7-8784-c02650a9bdc6");
|
|
static QUuid mockEnergyStorageThingClassId = QUuid("d0d5bbf0-249c-46ed-ac6a-5f271b2b0b0f");
|
|
static QUuid mockPowerSwitchThingClassId = QUuid("841f8905-d1d7-4053-909f-01123b497747");
|
|
//! \brief Relais SEC — il commute, il ne mesure pas. Ouvre le cas « charge saine et NON
|
|
//! mesurée », que le mock ne savait pas produire : toutes les classes pilotables portaient une
|
|
//! puissance, si bien qu'aucun test ne pouvait distinguer « non mesurée » de « en défaut »
|
|
//! (docs/INVENTAIRE_MOCK.md).
|
|
static QUuid mockDryRelayThingClassId = QUuid("5b2c7e41-9a06-4d3f-b8c1-6e0f2a7d4b39");
|
|
|
|
using namespace nymeaserver;
|
|
|
|
class EnergyTestBase : public NymeaTestBase
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
explicit EnergyTestBase(QObject *parent = nullptr);
|
|
|
|
inline void verifyThingError(const QVariant &response, Thing::ThingError error = Thing::ThingErrorNoError) {
|
|
verifyError(response, "thingError", enumValueName(error));
|
|
}
|
|
|
|
inline void verifyEnergyError(const QVariant &response, EnergyManager::EnergyError error = EnergyManager::EnergyErrorNoError) {
|
|
verifyError(response, "energyError", enumValueName(error));
|
|
}
|
|
|
|
static QDateTime utcDateTime(const QDate &date, const QTime &time);
|
|
|
|
protected slots:
|
|
void initTestCase(const QString &loggingRules = QString(), bool checkExperienceLoaded = true, bool disableLogEngine = true);
|
|
void cleanupTestCase();
|
|
|
|
void init();
|
|
void cleanup();
|
|
|
|
public:
|
|
QNetworkReply *setMeterStates(const QVariantMap &phasesPower, bool connected = true, quint16 port = 26655);
|
|
QNetworkReply *setCarStates(uint batteryLevel, uint capacity, uint minChargingCurrent, uint phaseCount, quint16 port = 26656);
|
|
QNetworkReply *setChargerStates(bool connected, bool power, bool pluggedIn, const QString &phases, int maxChargingCurrent, int maxChargingCurrentMaxValue, quint16 port = 26657);
|
|
QNetworkReply *setChargerWithPhaseCountSwitchingStates(bool connected, bool power, bool pluggedIn, const QString &phases, int maxChargingCurrent, int maxChargingCurrentMaxValue, uint desiredPhaseCount, quint16 port = 26658);
|
|
QNetworkReply *setSimpleChargerStates(bool connected, bool power, bool pluggedIn, int phaseCount, int maxChargingCurrent, quint16 port = 26659);
|
|
QNetworkReply *setEnergyStorageStates(uint batteryLevel, int currentPower, quint16 port = 26660);
|
|
QNetworkReply *setPowerSwitchStates(bool power, double currentPower, quint16 port = 26661);
|
|
|
|
QNetworkReply *getActionHistory(quint16 port);
|
|
QNetworkReply *clearActionHistroy(quint16 port);
|
|
|
|
QUuid addMeter(quint16 port = 26655);
|
|
|
|
//! \brief Ajoute un compteur SANS `currentPhaseB/C` ni `voltagePhaseC`.
|
|
//! \param port Port du contrôleur mock. \return L'identifiant du Thing créé.
|
|
QUuid addMeterPowerOnly(quint16 port = 26690);
|
|
QUuid addCar(quint16 port = 26656);
|
|
QUuid addCharger(const QString &phases = "All", double maxChargingCurrentUpperLimit = 32, quint16 port = 26657);
|
|
QUuid addChargerWithPhaseCountSwitching(const QString &phases = "All", double maxChargingCurrentUpperLimit = 32, quint16 port = 26658);
|
|
QUuid addSimpleCharger(double maxChargingCurrentUpperLimit = 32, quint16 port = 26659);
|
|
QUuid addEnergyStorage(uint capacity = 10, double maxChargingPowerUpperLimit = 5000, double maxDischargingPowerUpperLimit = 11500, quint16 port = 26660);
|
|
QUuid addPowerSwitch(double nominalPower = 2000, quint16 port = 26661);
|
|
|
|
/*!
|
|
* \brief Ajoute un RELAIS SEC : il commute, il ne mesure pas.
|
|
* \param port Port d'identification (aucun contrôleur n'écoute — la classe n'en a pas besoin).
|
|
* \return L'identifiant du Thing créé.
|
|
* \note Sans cette classe, une charge « saine mais non mesurée » était impossible à
|
|
* fabriquer, et `measurement.source: none` n'était atteignable qu'avec un Thing MANQUANT —
|
|
* ce qui traîne `available: false` et mélange deux affirmations distinctes.
|
|
*/
|
|
QUuid addDryRelay(quint16 port = 26680);
|
|
//! Mock thing implémentant l'interface etmvariableload (states currentPowerW / powerSetpoint).
|
|
QUuid addEtmVariableLoad(quint16 port = 27001);
|
|
|
|
void removeDevices();
|
|
QVariant removeDevice(const QUuid &thingId);
|
|
|
|
protected:
|
|
QNetworkAccessManager *m_networkAccessManager = nullptr;
|
|
|
|
quint16 m_mockMeterDefaultPort = 26655;
|
|
quint16 m_mockCarDefaultPort = 26656;
|
|
quint16 m_mockChargerDefaultPort = 26657;
|
|
quint16 m_mockChargerWithPhaseCountSwitchingDefaultPort = 26658;
|
|
quint16 m_mockSimpleChargerDefaultPort = 26659;
|
|
quint16 m_mockEnergyStorageDefaultPort = 26660;
|
|
quint16 m_mockPowerSwitchDefaultPort = 26661;
|
|
|
|
bool verifyActionExecuted(const QVariantList &actionHistory, const QString &actionName);
|
|
QVariant getLastValueFromExecutedAction(const QVariantList &actionHistory, const QString &actionName, const QString ¶mName);
|
|
|
|
};
|
|
|
|
#endif // ENERGYTESTBASE_H
|