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).
|
||||
- `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.
|
||||
- `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.
|
||||
- `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;
|
||||
if (m_em && m_em->rootMeter())
|
||||
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).
|
||||
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());
|
||||
if (phasePowerLimit() > 0)
|
||||
fil.phaseLimitA = {double(phasePowerLimit()), double(phasePowerLimit()), double(phasePowerLimit())};
|
||||
|
||||
@ -47,9 +47,10 @@ struct ContexteFil {
|
||||
QString trigger = QStringLiteral("cycle");
|
||||
//! ThingId du compteur racine (\c meter.source) ; vide = inconnu → \c null.
|
||||
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
|
||||
//! « nuit » de « pas d'onduleur ».
|
||||
//! « nuit » de « pas de producteur ».
|
||||
std::optional<double> pvCurrentW;
|
||||
//! Limite par phase en A (\c site.phaseLimitA) ; vide = celle du contexte, sinon \c null.
|
||||
QList<double> phaseLimitA;
|
||||
|
||||
@ -54,6 +54,11 @@ public:
|
||||
|
||||
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:
|
||||
void updatePowerBalance();
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
// à un relais de 1000 W (rang 1), un routeur etmvariableload de 3000 W (rang 2).
|
||||
|
||||
#include "simulationbase.h"
|
||||
#include "experience/energymanagermock.h"
|
||||
#include "../common/etm-powersync-fakeoptimizer.h"
|
||||
|
||||
#include "energysettings.h"
|
||||
@ -88,6 +89,7 @@ private slots:
|
||||
void testANewInvalidPlanReplacesTheCache();
|
||||
void testConnectionLossDropsThePlan();
|
||||
void testThePlanOfAnOptimizerBuysNothingEco();
|
||||
void testPvIsReadFromAnyProducer();
|
||||
|
||||
private:
|
||||
//! 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é
|
||||
}
|
||||
|
||||
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)
|
||||
#include "testsocketscheduler.moc"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user