fix(optimiseur): pv.currentW publié dès qu'un smartmeterproducer existe, pas seulement un solarinverter
`solarinverter` étend `smartmeterproducer` dans libnymea. Un compteur de production posé sur un onduleur non communicant n'est que `smartmeterproducer` — c'est le PV du banc (sdm120Producer) : le filtre précédent y rendait `pv.currentW: null` alors que l'EnergyManager additionnait bien sa production. Même critère que la présence d'un producteur côté apprentissage. - OPTIMIZER_PROTOCOL.md §5.1 : `pv.currentW` documenté ; un seul producteur déclaré par source PV (nymea additionne les producteurs). - EnergyManagerMock::setCurrentPowerProduction() (test) : la production du mock valait toujours 0. - Test testPvIsReadFromAnyProducer : sans producteur → null ; smartmeterproducer seul → 2400. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6481515b64
commit
c81c7b6225
@ -191,6 +191,13 @@ Réponse attendue :
|
|||||||
immédiatement (§4).
|
immédiatement (§4).
|
||||||
- `meter.source` (1.1) : identité du compteur racine désigné (`Energy.SetRootMeter`), unique.
|
- `meter.source` (1.1) : identité du compteur racine désigné (`Energy.SetRootMeter`), unique.
|
||||||
- `meter.exportW` est la mesure **brute** (`AGENTS.md` règle 8) : jamais le net après pilotage.
|
- `meter.exportW` est la mesure **brute** (`AGENTS.md` règle 8) : jamais le net après pilotage.
|
||||||
|
- `pv.currentW` (1.1) : production instantanée du site en W, **positive**, telle que l'additionne
|
||||||
|
l'`EnergyManager` de nymea (`|currentPowerProduction|`). Publiée dès qu'au moins un Thing
|
||||||
|
`smartmeterproducer` est configuré — onduleur communicant (`solarinverter`, qui l'étend) **ou**
|
||||||
|
compteur de production posé sur un onduleur non communicant. Sans producteur : `null`, jamais
|
||||||
|
0 (0 W ne distinguerait pas la nuit de l'absence de PV). **Un seul producteur déclaré par
|
||||||
|
source PV** : nymea additionne tous les producteurs, et un onduleur communicant doublé d'un
|
||||||
|
compteur de production sur la même chaîne compterait la même énergie deux fois.
|
||||||
- `site.gridCapW` : réservé (signaux réseau type §14a EnWG). `null` en France.
|
- `site.gridCapW` : réservé (signaux réseau type §14a EnWG). `null` en France.
|
||||||
- `tariff` (1.0) reste émis pour compatibilité ; un optimiseur 1.1 lit `forecast.price`.
|
- `tariff` (1.0) reste émis pour compatibilité ; un optimiseur 1.1 lit `forecast.price`.
|
||||||
|
|
||||||
|
|||||||
@ -2876,9 +2876,15 @@ OptimizerProtocol::ContexteFil EnergyArbitrator::contexteFil(const SurplusContex
|
|||||||
OptimizerProtocol::ContexteFil fil;
|
OptimizerProtocol::ContexteFil fil;
|
||||||
if (m_em && m_em->rootMeter())
|
if (m_em && m_em->rootMeter())
|
||||||
fil.meterSource = m_em->rootMeter()->id().toString();
|
fil.meterSource = m_em->rootMeter()->id().toString();
|
||||||
// La PV instantanée n'a de sens qu'avec un onduleur : sans lui, 0 W ne distinguerait pas la
|
// La PV instantanée n'a de sens qu'avec un PRODUCTEUR : sans lui, 0 W ne distinguerait pas la
|
||||||
// nuit de l'absence de production — le champ reste alors inconnu (null).
|
// nuit de l'absence de production — le champ reste alors inconnu (null).
|
||||||
if (m_em && m_tm && !m_tm->configuredThings().filterByInterface(QStringLiteral("solarinverter")).isEmpty())
|
//
|
||||||
|
// `smartmeterproducer`, pas `solarinverter` : le second ÉTEND le premier dans libnymea, et un
|
||||||
|
// compteur de production posé sur un onduleur non communicant n'est que `smartmeterproducer`
|
||||||
|
// — c'est le PV du banc (sdm120Producer). Filtrer sur `solarinverter` y rendait `null` alors
|
||||||
|
// que l'EnergyManager additionnait bien sa production. Même critère que « le site a-t-il un
|
||||||
|
// producteur ? » de l'apprentissage (observerPourApprentissage()).
|
||||||
|
if (m_em && m_tm && !m_tm->configuredThings().filterByInterface(QStringLiteral("smartmeterproducer")).isEmpty())
|
||||||
fil.pvCurrentW = qAbs(m_em->currentPowerProduction());
|
fil.pvCurrentW = qAbs(m_em->currentPowerProduction());
|
||||||
if (phasePowerLimit() > 0)
|
if (phasePowerLimit() > 0)
|
||||||
fil.phaseLimitA = {double(phasePowerLimit()), double(phasePowerLimit()), double(phasePowerLimit())};
|
fil.phaseLimitA = {double(phasePowerLimit()), double(phasePowerLimit()), double(phasePowerLimit())};
|
||||||
|
|||||||
@ -47,9 +47,10 @@ struct ContexteFil {
|
|||||||
QString trigger = QStringLiteral("cycle");
|
QString trigger = QStringLiteral("cycle");
|
||||||
//! ThingId du compteur racine (\c meter.source) ; vide = inconnu → \c null.
|
//! ThingId du compteur racine (\c meter.source) ; vide = inconnu → \c null.
|
||||||
QString meterSource;
|
QString meterSource;
|
||||||
//! Production PV instantanée (\c pv.currentW) ; absente = inconnue → \c null. Le champ
|
//! Production PV instantanée (\c pv.currentW) ; absente = inconnue → \c null. Remplie dès
|
||||||
|
//! qu'un Thing \c smartmeterproducer existe (onduleur ou compteur de production). Le champ
|
||||||
//! \c SurplusContext::pv n'est pas lu : il n'est jamais rempli, et son 0 ne distingue pas
|
//! \c SurplusContext::pv n'est pas lu : il n'est jamais rempli, et son 0 ne distingue pas
|
||||||
//! « nuit » de « pas d'onduleur ».
|
//! « nuit » de « pas de producteur ».
|
||||||
std::optional<double> pvCurrentW;
|
std::optional<double> pvCurrentW;
|
||||||
//! Limite par phase en A (\c site.phaseLimitA) ; vide = celle du contexte, sinon \c null.
|
//! Limite par phase en A (\c site.phaseLimitA) ; vide = celle du contexte, sinon \c null.
|
||||||
QList<double> phaseLimitA;
|
QList<double> phaseLimitA;
|
||||||
|
|||||||
@ -54,6 +54,11 @@ public:
|
|||||||
|
|
||||||
EnergyLogs* logs() const override;
|
EnergyLogs* logs() const override;
|
||||||
|
|
||||||
|
//! \brief Pose la production instantanée (W, négative = produite, convention nymea).
|
||||||
|
//! Le bilan de ce mock n'est jamais recalculé depuis les Things : sans ce réglage, la
|
||||||
|
//! production vaut 0 et aucun test ne peut distinguer « producteur à 0 W » de « mesuré ».
|
||||||
|
void setCurrentPowerProduction(double watts) { m_currentPowerProduction = watts; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updatePowerBalance();
|
void updatePowerBalance();
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
// à un relais de 1000 W (rang 1), un routeur etmvariableload de 3000 W (rang 2).
|
// à un relais de 1000 W (rang 1), un routeur etmvariableload de 3000 W (rang 2).
|
||||||
|
|
||||||
#include "simulationbase.h"
|
#include "simulationbase.h"
|
||||||
|
#include "experience/energymanagermock.h"
|
||||||
#include "../common/etm-powersync-fakeoptimizer.h"
|
#include "../common/etm-powersync-fakeoptimizer.h"
|
||||||
|
|
||||||
#include "energysettings.h"
|
#include "energysettings.h"
|
||||||
@ -88,6 +89,7 @@ private slots:
|
|||||||
void testANewInvalidPlanReplacesTheCache();
|
void testANewInvalidPlanReplacesTheCache();
|
||||||
void testConnectionLossDropsThePlan();
|
void testConnectionLossDropsThePlan();
|
||||||
void testThePlanOfAnOptimizerBuysNothingEco();
|
void testThePlanOfAnOptimizerBuysNothingEco();
|
||||||
|
void testPvIsReadFromAnyProducer();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
//! Banc commun ; \p avecOptimiseur faux = rien n'est configuré.
|
//! Banc commun ; \p avecOptimiseur faux = rien n'est configuré.
|
||||||
@ -698,6 +700,36 @@ void TestSocketScheduler::testThePlanOfAnOptimizerBuysNothingEco()
|
|||||||
QCOMPARE(p.value("purchasedWh").toDouble(), 0.0); // …et l'éco n'en a rien acheté
|
QCOMPARE(p.value("purchasedWh").toDouble(), 0.0); // …et l'éco n'en a rien acheté
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestSocketScheduler::testPvIsReadFromAnyProducer()
|
||||||
|
{
|
||||||
|
// `pv.currentW` se lit dès qu'un `smartmeterproducer` existe — pas seulement un
|
||||||
|
// `solarinverter`, qui l'ÉTEND dans libnymea. Le producteur du mock est exactement le cas
|
||||||
|
// du banc : un compteur de production (sdm120Producer) posé sur un onduleur non
|
||||||
|
// communicant, `smartmeterproducer` et rien d'autre.
|
||||||
|
monter();
|
||||||
|
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||||
|
EnergyManagerMock *em = dynamic_cast<EnergyManagerMock *>(m_experiencePlugin->energyManager());
|
||||||
|
QVERIFY(em);
|
||||||
|
em->setCurrentPowerProduction(-2400); // convention nymea : produite = négative
|
||||||
|
|
||||||
|
// Sans producteur : inconnu (null), même si l'EnergyManager rend un chiffre — 0 W ne
|
||||||
|
// distinguerait pas la nuit de l'absence de PV.
|
||||||
|
cycle(t0, -3000);
|
||||||
|
QVariantMap pv = m_requetes.last().value("pv").toMap();
|
||||||
|
QVERIFY(pv.contains("currentW"));
|
||||||
|
QVERIFY(pv.value("currentW").isNull());
|
||||||
|
|
||||||
|
const QUuid producteur = addSolarInverter();
|
||||||
|
Thing *t = NymeaCore::instance()->thingManager()->findConfiguredThing(producteur);
|
||||||
|
QVERIFY(t);
|
||||||
|
QVERIFY2(t->thingClass().interfaces().contains("smartmeterproducer")
|
||||||
|
&& !t->thingClass().interfaces().contains("solarinverter"),
|
||||||
|
"le cas éprouvé est un producteur qui N'EST PAS un onduleur");
|
||||||
|
cycle(t0.addSecs(60), -3000);
|
||||||
|
pv = m_requetes.last().value("pv").toMap();
|
||||||
|
QVERIFY2(!pv.value("currentW").isNull(), "un smartmeterproducer seul doit suffire");
|
||||||
|
QCOMPARE(pv.value("currentW").toDouble(), 2400.0);
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_MAIN(TestSocketScheduler)
|
QTEST_MAIN(TestSocketScheduler)
|
||||||
#include "testsocketscheduler.moc"
|
#include "testsocketscheduler.moc"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user