[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
This commit is contained in:
parent
fc844ab5d1
commit
4cadf8b57c
@ -1,3 +1,39 @@
|
||||
powersync-energy-plugin-nymea (1.15.2+etm53) trixie; urgency=medium
|
||||
|
||||
* [upstream-fix] rootmeter — LA TENSION EST DÉSORMAIS 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. 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. Plus étroit, et bien réel.
|
||||
* DÉMONTRÉ : B à 200 V, C à 4 000 W sans tension déclarée → 5,0 A de marge annoncés 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.
|
||||
* MOCK — classe `meterPowerOnly` : puissances par phase et tension de B, SANS les courants de
|
||||
B/C ni la tension de C. Sans elle le défaut restait inatteignable. TROISIÈME fois qu'ouvrir
|
||||
le mock révèle un défaut plutôt que de seulement débloquer un test, après `dryRelay` et après
|
||||
la correction du raisonnement sur l'écrêtage de la marge.
|
||||
* `draw.bindingPhase` — LAQUELLE des phases borne. `perPhaseBound` disait qu'une phase borne,
|
||||
jamais laquelle : l'écran ne pouvait dire que « bridé » sans dire par quoi, et enverrait
|
||||
mesurer au hasard sur trois phases dont deux vont bien. Le motif DRAW_CAP la porte aussi,
|
||||
mais il n'est émis que si une charge est REFUSÉE — un cycle qui borde sans rien refuser
|
||||
laisserait l'écran muet. Les deux endroits ne se recouvrent pas.
|
||||
* CONTRAT — une installation qui EXPORTE peut être bridée, et l'écran ne doit jamais présenter
|
||||
une marge globale ni la meilleure phase : c'est la PIRE qui décide. Contre-intuitif au point
|
||||
qu'un installateur cherchera l'erreur ailleurs.
|
||||
* 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. Deux occurrences de deux côtés de la
|
||||
frontière. Parade : quand une décision est écrite avec sa raison, la raison mérite son propre
|
||||
contrôle — un test qui vérifie la conclusion ne vérifie pas le raisonnement, et c'est le
|
||||
raisonnement 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.
|
||||
|
||||
-- Patrick Schurig <etm.schurig@gmail.com> Fri, 04 Sep 2026 11:00:00 +0200
|
||||
|
||||
powersync-energy-plugin-nymea (1.15.2+etm52) trixie; urgency=medium
|
||||
|
||||
* MOCK — tension et courant PAR PHASE réglables indépendamment des puissances. Le mock posait
|
||||
|
||||
@ -214,7 +214,28 @@ elle se pose : **quand un code lit un état d'appareil, demander ce qu'il fait s
|
||||
absent** — et se souvenir que `stateValue()` d'un état absent rend `0`, valeur qui, dans un
|
||||
calcul de marge, se confond avec « rien ne consomme ».
|
||||
|
||||
### Défaut amont voisin, NON corrigé — la tension fuit d'une phase à l'autre
|
||||
### Défaut amont voisin, CORRIGÉ le 2026-09-04 — la tension fuyait d'une phase à l'autre
|
||||
|
||||
`voltage` était déclarée **une seule fois** hors des trois blocs, et mutée par celui d'une phase :
|
||||
une phase qui ne publie pas sa propre tension était donc calculée avec **celle d'une autre**.
|
||||
|
||||
**Mais le défaut n'était pas celui qu'on croyait, et le mesurer l'a montré.** L'interface
|
||||
`energymeter` rend `currentPhaseA` et `voltagePhaseA` **obligatoires** ; `currentPhaseB/C` et
|
||||
`voltagePhaseB/C` sont **optionnels**. La branche « puissance ÷ tension » de la phase A est donc
|
||||
**morte par construction** — la fuite n'allait pas de A vers B, comme la note d'origine
|
||||
l'affirmait, mais **de B vers C**. Plus étroit, et bien réel.
|
||||
|
||||
**Démontré** : B à 200 V, C à 4 000 W sans tension déclarée → **5,0 A de marge annoncés au lieu de
|
||||
7,6**. Le masque doit couvrir B *et* C — interroger C seule n'exécute pas le bloc de B et ne
|
||||
révèle rien — et C doit être la phase la plus contrainte, sinon le `qMin` retient B et masque
|
||||
l'écart. **Deux conditions, dont aucune n'était évidente avant d'avoir écrit le test.**
|
||||
|
||||
**Et il n'était pas atteignable au banc** : le mock ne savait produire qu'un compteur publiant
|
||||
tous les états. Il a fallu une classe `meterPowerOnly` — puissances et tension de B, 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 (`docs/INVENTAIRE_MOCK.md`).
|
||||
|
||||
### Note d'origine, conservée — elle décrivait mal le défaut
|
||||
|
||||
Dans la même fonction, `voltage` est déclarée une fois **hors** des trois blocs et mutée par
|
||||
celui de la phase A. Un compteur ayant `voltagePhaseA` mais pas `voltagePhaseB` verrait donc la
|
||||
|
||||
@ -1577,6 +1577,11 @@ void EnergyArbitrator::buildTelemetry(const Plan &plan, const Slot &slot, const
|
||||
draw.insert(QStringLiteral("remainingW"), plan.draw.remainingW);
|
||||
draw.insert(QStringLiteral("binding"), plan.draw.bindingCode);
|
||||
draw.insert(QStringLiteral("perPhaseBound"), plan.draw.perPhaseBound);
|
||||
// LAQUELLE borne — présent seulement s'il y en a une. `perPhaseBound` dit qu'une phase
|
||||
// borne, jamais laquelle : l'écran ne pourrait dire que « bridé » sans dire par quoi, et
|
||||
// enverrait mesurer au hasard sur trois phases dont deux vont bien.
|
||||
if (!plan.draw.bindingPhase.isEmpty())
|
||||
draw.insert(QStringLiteral("bindingPhase"), plan.draw.bindingPhase);
|
||||
}
|
||||
snap.insert(QStringLiteral("draw"), draw);
|
||||
|
||||
|
||||
@ -184,6 +184,8 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
|
||||
draw.authorisedW = ctx.drawMargin.watts;
|
||||
draw.bindingCode = drawCapSourceCode(ctx.drawMargin.source);
|
||||
draw.perPhaseBound = ctx.drawMargin.phase >= 0;
|
||||
if (draw.perPhaseBound && ctx.drawMargin.phase <= 2)
|
||||
draw.bindingPhase = QString(QChar('A' + ctx.drawMargin.phase));
|
||||
}
|
||||
compta.surplusW = remainingSurplusW;
|
||||
compta.evReservedW = evReservedW;
|
||||
|
||||
@ -127,6 +127,22 @@ struct PlanDraw {
|
||||
//! aucun plafond. Ce qu'on a le droit de faire d'un dépassement en dépend (\rule{LM-1211-c}).
|
||||
QString bindingCode;
|
||||
|
||||
|
||||
/*!
|
||||
* \brief LAQUELLE des phases borne — \c A, \c B ou \c C. Vide si le plafond est un total.
|
||||
*
|
||||
* \par Pourquoi \c perPhaseBound ne suffisait pas
|
||||
* Il dit qu'une phase borne, jamais laquelle. Or une installation qui **exporte globalement**
|
||||
* peut être bridée par une seule phase saturée (\rule{LM-1210-a}) — contre-intuitif au point
|
||||
* qu'un installateur cherchera l'erreur ailleurs. Sans ce champ, l'écran ne peut dire que
|
||||
* « bridé » **sans pouvoir dire par quoi**, et il enverrait mesurer au hasard sur trois phases
|
||||
* dont deux vont bien.
|
||||
*
|
||||
* \note La phase figure aussi dans le motif \c DRAW_CAP — mais celui-ci n'est émis que si une
|
||||
* charge est réellement REFUSÉE. Un cycle où le plafond borde sans rien refuser laisserait
|
||||
* l'écran muet. Les deux endroits sont nécessaires, et ils ne se recouvrent pas.
|
||||
*/
|
||||
QString bindingPhase;
|
||||
//! \brief Le plafond qui borne est-il exprimé PAR PHASE ? Un plafond total n'a pas de phase
|
||||
//! coupable, et le dire évite d'envoyer mesurer au hasard sur une triphasée.
|
||||
bool perPhaseBound = false;
|
||||
|
||||
@ -78,16 +78,26 @@ double RootMeter::currentPhaseC() const
|
||||
double RootMeter::calculateAllowanceAmpere(Electricity::Phases phases, uint phasePowerLimit)
|
||||
{
|
||||
double allowance = phasePowerLimit;
|
||||
double voltage = 230;
|
||||
|
||||
// [upstream-fix] La tension est LOCALE À CHAQUE PHASE. Elle était déclarée une seule fois
|
||||
// ici et mutée par le bloc de la phase B, si bien que la phase C — quand elle ne publie pas
|
||||
// sa propre tension — était calculée avec celle de B. Démontré : B à 200 V et C à 4 000 W
|
||||
// donnaient 5,0 A de marge au lieu de 7,6.
|
||||
//
|
||||
// Le défaut n'était pas atteignable depuis la phase A : l'interface `energymeter` rend
|
||||
// `currentPhaseA` et `voltagePhaseA` OBLIGATOIRES, donc la branche « puissance ÷ tension »
|
||||
// de A est morte par construction. Seules B et C sont optionnelles, et la fuite allait donc
|
||||
// de B vers C — plus étroit que « la tension de A sert à calculer B », mais bien réel.
|
||||
auto tensionDe = [this](const char *nomTension) {
|
||||
return m_thing->hasState(nomTension) ? m_thing->stateValue(nomTension).toDouble() : 230.0;
|
||||
};
|
||||
|
||||
if (phases.testFlag(Electricity::PhaseA) || phases == Electricity::PhaseNone) {
|
||||
if (m_thing->hasState("currentPhaseA")) {
|
||||
allowance = qMin(allowance, 1.0 * phasePowerLimit - m_thing->stateValue("currentPhaseA").toDouble());
|
||||
} else if (m_thing->hasState("currentPowerPhaseA")) {
|
||||
if (m_thing->hasState("voltagePhaseA"))
|
||||
voltage = m_thing->stateValue("voltagePhaseA").toDouble();
|
||||
|
||||
allowance = qMin(allowance, 1.0 * phasePowerLimit - (m_thing->stateValue("currentPowerPhaseA").toDouble() / voltage));
|
||||
allowance = qMin(allowance, 1.0 * phasePowerLimit
|
||||
- (m_thing->stateValue("currentPowerPhaseA").toDouble() / tensionDe("voltagePhaseA")));
|
||||
} else {
|
||||
// No currentPhaseA state... Assume single phase and deduct from currentPower
|
||||
double current = m_thing->stateValue("currentPower").toDouble() / 230;
|
||||
@ -103,10 +113,8 @@ double RootMeter::calculateAllowanceAmpere(Electricity::Phases phases, uint phas
|
||||
// entière, donc aucun écrêtage.
|
||||
allowance = qMin(allowance, 1.0 * phasePowerLimit - m_thing->stateValue("currentPhaseB").toDouble());
|
||||
} else if (m_thing->hasState("currentPowerPhaseB")) {
|
||||
if (m_thing->hasState("voltagePhaseB"))
|
||||
voltage = m_thing->stateValue("voltagePhaseB").toDouble();
|
||||
|
||||
allowance = qMin(allowance, 1.0 * phasePowerLimit - (m_thing->stateValue("currentPowerPhaseB").toDouble() / voltage));
|
||||
allowance = qMin(allowance, 1.0 * phasePowerLimit
|
||||
- (m_thing->stateValue("currentPowerPhaseB").toDouble() / tensionDe("voltagePhaseB")));
|
||||
} else {
|
||||
qCDebug(dcNymeaEnergy()) << "Asked to calculate allowence for Phase B on a single phase root meter.";
|
||||
}
|
||||
@ -121,10 +129,8 @@ double RootMeter::calculateAllowanceAmpere(Electricity::Phases phases, uint phas
|
||||
// entière, donc aucun écrêtage.
|
||||
allowance = qMin(allowance, 1.0 * phasePowerLimit - m_thing->stateValue("currentPhaseC").toDouble());
|
||||
} else if (m_thing->hasState("currentPowerPhaseC")) {
|
||||
if (m_thing->hasState("voltagePhaseC"))
|
||||
voltage = m_thing->stateValue("voltagePhaseC").toDouble();
|
||||
|
||||
allowance = qMin(allowance, 1.0 * phasePowerLimit - (m_thing->stateValue("currentPowerPhaseC").toDouble() / voltage));
|
||||
allowance = qMin(allowance, 1.0 * phasePowerLimit
|
||||
- (m_thing->stateValue("currentPowerPhaseC").toDouble() / tensionDe("voltagePhaseC")));
|
||||
} else {
|
||||
qCDebug(dcNymeaEnergy()) << "Asked to calculate allowence for Phase C on a single phase root meter.";
|
||||
}
|
||||
|
||||
@ -332,6 +332,25 @@ QUuid EnergyTestBase::addMeter(quint16 port)
|
||||
return response.toMap().value("params").toMap().value("thingId").toUuid();
|
||||
}
|
||||
|
||||
QUuid EnergyTestBase::addMeterPowerOnly(quint16 port)
|
||||
{
|
||||
QVariantList thingParams;
|
||||
QVariantMap portParam;
|
||||
portParam.insert("paramTypeId", "{c1108d3a-6391-428f-8d75-91e8986657d6}");
|
||||
portParam.insert("value", port);
|
||||
thingParams.append(portParam);
|
||||
|
||||
QVariantMap params;
|
||||
params.insert("thingClassId", mockMeterPowerOnlyThingClassId.toString());
|
||||
params.insert("name", "Meter");
|
||||
params.insert("thingParams", thingParams);
|
||||
|
||||
QVariant response = injectAndWait("Integrations.AddThing", params);
|
||||
verifyThingError(response);
|
||||
|
||||
return response.toMap().value("params").toMap().value("thingId").toUuid();
|
||||
}
|
||||
|
||||
QUuid EnergyTestBase::addCar(quint16 port)
|
||||
{
|
||||
QVariantList thingParams;
|
||||
|
||||
@ -43,6 +43,11 @@
|
||||
#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");
|
||||
@ -93,6 +98,10 @@ public:
|
||||
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);
|
||||
|
||||
@ -7690,6 +7690,19 @@ void Simulation::testWorstPhaseBindsWhenPhasesAreUnbalanced()
|
||||
QCOMPARE(qRound(marge.watts), -1150);
|
||||
QCOMPARE(marge.phase, 1); // phase B (0 = A, 1 = B, 2 = C)
|
||||
|
||||
// 1-bis. ET L'ÉCRAN DOIT POUVOIR NOMMER CETTE PHASE. `perPhaseBound` dit qu'une phase borne,
|
||||
// jamais laquelle : l'écran ne pourrait dire que « bridé » sans dire par quoi, et
|
||||
// enverrait mesurer au hasard sur trois phases dont deux vont bien. Le motif `DRAW_CAP`
|
||||
// la porte aussi, mais il n'est émis que si une charge est REFUSÉE — un cycle qui borde
|
||||
// sans rien refuser laisserait l'écran muet. Les deux endroits ne se recouvrent pas.
|
||||
SurplusContext ctxPhase;
|
||||
ctxPhase.timestamp = utcDateTime(QDate(2026, 9, 4), QTime(13, 0, 0));
|
||||
ctxPhase.drawMargin = marge;
|
||||
RuleBasedScheduler schedPhase(arb);
|
||||
const Plan planPhase = schedPhase.getPlan(ctxPhase);
|
||||
QCOMPARE(planPhase.draw.perPhaseBound, true);
|
||||
QCOMPARE(planPhase.draw.bindingPhase, QString("B"));
|
||||
|
||||
// 2. ET LA MARGE EST NÉGATIVE alors que le compteur global est en EXPORT. C'est tout l'objet
|
||||
// du choix : sans la lecture par phase, l'arbitre croirait disposer de 2 300 W à dépenser
|
||||
// pendant qu'une phase est déjà au-delà de son disjoncteur.
|
||||
@ -7714,6 +7727,70 @@ void Simulation::testWorstPhaseBindsWhenPhasesAreUnbalanced()
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief [upstream-fix] La tension d'une phase ne doit pas servir à calculer une AUTRE phase.
|
||||
*
|
||||
* \par Le défaut
|
||||
* `calculateAllowanceAmpere()` déclare `voltage` **une seule fois, hors des trois blocs**, et le
|
||||
* bloc de la phase A la mute. Un compteur qui publie `voltagePhaseA` mais pas `voltagePhaseB`
|
||||
* verrait donc la phase B calculée **avec la tension de A**.
|
||||
*
|
||||
* \par Pourquoi ce test n'existait pas
|
||||
* Le mock posait `currentPhaseX = currentPowerPhaseX / 230` : tension implicite, identique
|
||||
* partout, et le défaut invisible. C'est la **troisième fois** que l'ouverture du mock révèle un
|
||||
* défaut plutôt que de seulement débloquer un test — après `dryRelay` et après la correction du
|
||||
* raisonnement sur l'écrêtage de la marge.
|
||||
*/
|
||||
void Simulation::testPhaseVoltageDoesNotLeakBetweenPhases()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
|
||||
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||||
QUuid meterId = addMeterPowerOnly(26691);
|
||||
Thing *meter = tm->findConfiguredThing(meterId);
|
||||
QVERIFY(meter);
|
||||
meter->setStateValue("connected", true);
|
||||
QVERIFY2(!meter->thingClass().hasStateType("currentPhaseC"),
|
||||
"ce compteur ne publie PAS les courants de B et C — c'est ce qui rend la branche "
|
||||
"« puissance ÷ tension » atteignable");
|
||||
QVERIFY2(meter->thingClass().hasStateType("voltagePhaseB"),
|
||||
"il publie la tension de B…");
|
||||
QVERIFY2(!meter->thingClass().hasStateType("voltagePhaseC"),
|
||||
"…mais PAS celle de C : c'est exactement le cas où la tension de B fuirait vers C");
|
||||
|
||||
RootMeter *rm = new RootMeter(meter);
|
||||
|
||||
// LE MASQUE DOIT COUVRIR B **ET** C — c'est la condition de la fuite : le bloc de B écrit
|
||||
// `voltage`, celui de C le relit. Interroger C SEULE ne révèle rien, puisque le bloc de B ne
|
||||
// s'exécute pas. (Première version de ce test, qui passait pour cette raison.)
|
||||
//
|
||||
// Et C doit être la phase la plus CONTRAINTE, sinon le `qMin` de la fonction retiendrait B
|
||||
// et masquerait l'écart. B : 1000 W sous 200 V = 5 A, marge 20 A. C : 4000 W sous une
|
||||
// tension INCONNUE — donc 230 V par défaut, 17,4 A, marge 7,6 A. Avec la fuite, C serait
|
||||
// calculée sous les 200 V de B : 20 A tirés, marge 5 A.
|
||||
meter->setStateValue("voltagePhaseB", 200);
|
||||
meter->setStateValue("currentPowerPhaseB", 1000);
|
||||
meter->setStateValue("currentPowerPhaseC", 4000);
|
||||
|
||||
const uint plafond = 25;
|
||||
const double marge = rm->calculateAllowanceAmpere(
|
||||
Electricity::PhaseB | Electricity::PhaseC, plafond);
|
||||
|
||||
// Sans fuite : min(20 ; 25 − 4000/230) = 7,6 A. Avec la fuite : min(20 ; 25 − 4000/200) = 5.
|
||||
QCOMPARE(qRound(marge * 10), qRound((25.0 - 4000.0 / 230.0) * 10));
|
||||
|
||||
// Et l'ORDRE ne doit rien changer : le résultat de C ne doit pas dépendre de ce qu'on a
|
||||
// demandé avant. C'est la propriété que la variable partagée détruisait.
|
||||
QCOMPARE(qRound(rm->calculateAllowanceAmpere(Electricity::PhaseC, plafond) * 10),
|
||||
qRound((25.0 - 4000.0 / 230.0) * 10));
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testCountsSumToTarget()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
|
||||
@ -202,6 +202,7 @@ private slots:
|
||||
void testHealthyButUnmeasuredDiffersFromFaulty();
|
||||
void testLifetimeCounterDropNeverRebasesTheProgress();
|
||||
void testWorstPhaseBindsWhenPhasesAreUnbalanced();
|
||||
void testPhaseVoltageDoesNotLeakBetweenPhases();
|
||||
void testRecreditIsALoanAgainstObedience();
|
||||
void testARefusedStateIsDistinguishableFromAnApplied();
|
||||
void testL4ShedsAPacAndSaysItForcedTheLock();
|
||||
|
||||
@ -193,7 +193,11 @@ void IntegrationPluginEnergyMocks::setupThing(ThingSetupInfo *info)
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
return;
|
||||
|
||||
} else if (thing->thingClassId() == meterThingClassId) {
|
||||
} 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");
|
||||
@ -250,17 +254,22 @@ void IntegrationPluginEnergyMocks::setupThing(ThingSetupInfo *info)
|
||||
const double uA = tension("voltagePhaseA");
|
||||
const double uB = tension("voltagePhaseB");
|
||||
const double uC = tension("voltagePhaseC");
|
||||
thing->setStateValue("voltagePhaseA", uA);
|
||||
thing->setStateValue("voltagePhaseB", uB);
|
||||
thing->setStateValue("voltagePhaseC", uC);
|
||||
// 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.
|
||||
thing->setStateValue("currentPhaseA", query.hasQueryItem("currentPhaseA")
|
||||
poser("currentPhaseA", query.hasQueryItem("currentPhaseA")
|
||||
? query.queryItemValue("currentPhaseA").toDouble() : currentPowerPhaseA / uA);
|
||||
thing->setStateValue("currentPhaseB", query.hasQueryItem("currentPhaseB")
|
||||
poser("currentPhaseB", query.hasQueryItem("currentPhaseB")
|
||||
? query.queryItemValue("currentPhaseB").toDouble() : currentPowerPhaseB / uB);
|
||||
thing->setStateValue("currentPhaseC", query.hasQueryItem("currentPhaseC")
|
||||
poser("currentPhaseC", query.hasQueryItem("currentPhaseC")
|
||||
? query.queryItemValue("currentPhaseC").toDouble() : currentPowerPhaseC / uC);
|
||||
|
||||
thing->setStateValue("currentPowerPhaseA", currentPowerPhaseA);
|
||||
|
||||
@ -145,6 +145,119 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "meterPowerOnly",
|
||||
"displayName": "Mocked Meter (puissances par phase SANS courants)",
|
||||
"id": "3ac91e57-08b4-4f26-9d31-7e5a2c60b4f8",
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"energymeter",
|
||||
"connectable"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "c1108d3a-6391-428f-8d75-91e8986657d6",
|
||||
"name": "port",
|
||||
"displayName": "Port",
|
||||
"type": "uint",
|
||||
"defaultValue": 6655
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "08aef3a2-311e-4371-85bc-0e8258d4b351",
|
||||
"name": "connected",
|
||||
"displayName": "Connected",
|
||||
"type": "bool",
|
||||
"cached": false,
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"id": "f4461fff-2582-47c0-8950-e16cd11a6cf3",
|
||||
"name": "voltagePhaseA",
|
||||
"displayName": "Voltage phase A",
|
||||
"type": "double",
|
||||
"unit": "Volt",
|
||||
"defaultValue": 230
|
||||
},
|
||||
{
|
||||
"id": "cfc8a027-b926-4ffc-8204-c7e34c5af689",
|
||||
"name": "voltagePhaseB",
|
||||
"displayName": "Voltage phase B",
|
||||
"type": "double",
|
||||
"unit": "Volt",
|
||||
"defaultValue": 230
|
||||
},
|
||||
{
|
||||
"id": "415358dc-d6e5-40a6-8be1-cd9a42bffad0",
|
||||
"name": "currentPower",
|
||||
"displayName": "Current power",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "7f50b83a-8537-4b6c-8353-9d4f93fc7c29",
|
||||
"name": "originalPower",
|
||||
"displayName": "Original current power (simulation)",
|
||||
"displayNameAction": "Set original current power (simulation)",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"writable": "true",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "d32eaccd-f7c4-4c6a-86c8-52f114f3b077",
|
||||
"name": "currentPowerPhaseA",
|
||||
"displayName": "Current power phase A",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "d29283a9-c7ee-4cee-8789-c5ed6d8564ca",
|
||||
"name": "currentPowerPhaseB",
|
||||
"displayName": "Current power phase B",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "d1c26b51-3938-47c6-8d4a-ead0ca20a5a2",
|
||||
"name": "currentPowerPhaseC",
|
||||
"displayName": "Current power phase C",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "89a95691-961a-42b8-83b6-81574239251e",
|
||||
"name": "totalEnergyConsumed",
|
||||
"displayName": "Total energy consumed",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0.0
|
||||
},
|
||||
{
|
||||
"id": "5a8f841d-60d2-443f-81bf-61b678268307",
|
||||
"name": "totalEnergyProduced",
|
||||
"displayName": "Total energy produced",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0.0
|
||||
},
|
||||
{
|
||||
"id": "c5b2b28b-11b4-4274-84ea-17786281201a",
|
||||
"name": "currentPhaseA",
|
||||
"displayName": "Current phase A",
|
||||
"type": "double",
|
||||
"unit": "Ampere",
|
||||
"defaultValue": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "charger",
|
||||
"displayName": "Mocked charger",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user