feat(prévision PV): GetForecastSnapshot publie les corrections par plan (perField, D2)
Décision de Patrick du 2026-10-13. Le `corrections` du site reste
l'intersection (+ `ac-limit` si plafond) ; le détail se lit désormais en
donnée : `perField`, une entrée par plan sommé, `{id, corrections}`,
triée par id, présente seulement avec une courbe. Pas de booléen
« divergent ». Le journal garde son message.
- EnergyArbitrator::forecastSnapshot() : paramètre de sortie optionnel
`parPlan` (ForecastPlanLu) ;
- handler : `o:perField` déclaré et servi ;
- mock : classe `pvPlane` (état forecastCurveTransport) + addPvPlane() ;
premier test qui fait passer une COURBE par le RPC
(testTheSnapshotPublishesEachPlanesCorrections : divergence,
concordance, plafond, refus). Contre-épreuve : parPlan non rempli →
le test échoue (0 entrée au lieu de 2) ;
- OPTIMIZER_PROTOCOL §5.2, INTERFACE.md, design §30.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5468192cf8
commit
96148b9153
@ -1389,3 +1389,10 @@ restent l'état de leur date, et c'est ici qu'on lit ce qui les corrige.*
|
||||
Une prévision **dégradée** (température absente) se lit dans `corrections: []`, pas dans
|
||||
`conversion`. Correction **avant publication** (`OPTIMIZER_PROTOCOL.md` §0, 1.1) : aucun
|
||||
émetteur n'a jamais produit `fallback`.
|
||||
2. **Corrections PAR PLAN, en donnée.** Le §29 publiait l'intersection des corrections et ne
|
||||
disait la divergence qu'au journal. `GetForecastSnapshot` porte désormais `perField` : une
|
||||
entrée par plan sommé, `{id, corrections}`. Le `corrections` du site reste l'intersection
|
||||
(+ `ac-limit` si plafond). **Pas de booléen « divergent »** : la divergence se lit dans les
|
||||
données. `OPTIMIZER_PROTOCOL.md` §5.2 ajoute `corrections` aux entrées de `perField`. Le
|
||||
journal garde son message. Le mock porte une classe `pvPlane` : pour la première fois, un
|
||||
test fait passer une **courbe** par le RPC (trou n° 8 du rapport T1, refermé pour le snapshot).
|
||||
|
||||
@ -211,7 +211,8 @@ index par index ; `null` dans une case = valeur inconnue — **jamais 0, jamais
|
||||
"acLimitW":8000,
|
||||
"p50W":[ ... ],
|
||||
"perField":[
|
||||
{"id":"<thingId plan>","label":"Toit sud","peakW":6000,"tiltDeg":35,"azimuthDeg":0,"p50W":[ ... ]}
|
||||
{"id":"<thingId plan>","label":"Toit sud","peakW":6000,"tiltDeg":35,"azimuthDeg":0,
|
||||
"corrections":["temperature"],"p50W":[ ... ]}
|
||||
]
|
||||
},
|
||||
"baseload":{"model":"none","p50W":[null, ...]},
|
||||
@ -256,6 +257,10 @@ Règles :
|
||||
- `perField` : optionnel, affichage et diagnostic. Convention d'orientation (Open-Meteo,
|
||||
PVGIS) : `tiltDeg` 0 = horizontal, 90 = vertical ; `azimuthDeg` 0 = sud, −90 = est,
|
||||
+90 = ouest, ±180 = nord.
|
||||
- `corrections` (1.1) : corrections déclarées **par ce plan** (jamais `ac-limit`, qui est
|
||||
une correction de site). Le `corrections` de `pv` est l'**intersection** de celles des
|
||||
plans, plus `ac-limit` si un plafond est configuré : une divergence entre plans se lit
|
||||
**ici**, dans les données — aucun booléen ne la résume.
|
||||
- **`baseload`** : consommation **hors charges pilotées**. `model` : `profile` | `learned` |
|
||||
`none` ; `none` + tableau de `null` autorisé (installation neuve). Le plugin la calcule
|
||||
depuis le compteur racine moins les charges, chacune selon `measurement.source` (§5.3) ;
|
||||
|
||||
@ -2348,10 +2348,13 @@ bool EnergyArbitrator::setForecastSiteId(const QString &siteId, std::optional<in
|
||||
return true;
|
||||
}
|
||||
|
||||
ForecastRevision EnergyArbitrator::forecastSnapshot(QString &fault, QVariantMap &faultParams) const
|
||||
ForecastRevision EnergyArbitrator::forecastSnapshot(QString &fault, QVariantMap &faultParams,
|
||||
QList<ForecastPlanLu> *parPlan) const
|
||||
{
|
||||
fault.clear();
|
||||
faultParams.clear();
|
||||
if (parPlan)
|
||||
parPlan->clear();
|
||||
ForecastRevision vide;
|
||||
if (!m_tm)
|
||||
return vide;
|
||||
@ -2369,6 +2372,7 @@ ForecastRevision EnergyArbitrator::forecastSnapshot(QString &fault, QVariantMap
|
||||
|
||||
// LA COURBE VIENT DU CANAL, pas de la table. C'est toute la frontière.
|
||||
QList<ForecastRevision> plans;
|
||||
QList<ForecastPlanLu> lus;
|
||||
QString refus;
|
||||
for (Thing *thing : m_tm->configuredThings()) {
|
||||
if (thing->parentId().toString() != m_forecastSiteId
|
||||
@ -2381,6 +2385,7 @@ ForecastRevision EnergyArbitrator::forecastSnapshot(QString &fault, QVariantMap
|
||||
const ForecastRevision rev = ForecastStore::depuisTransport(charge, r);
|
||||
if (!r.isEmpty()) { refus = r; continue; }
|
||||
plans.append(rev);
|
||||
lus.append({thing->id().toString(), rev.corrections});
|
||||
}
|
||||
|
||||
// UNE CHARGE UTILE REFUSÉE N'EST PAS UNE PRÉVISION NULLE, et le motif existe pour que
|
||||
@ -2400,6 +2405,15 @@ ForecastRevision EnergyArbitrator::forecastSnapshot(QString &fault, QVariantMap
|
||||
if (site.points.isEmpty())
|
||||
return vide;
|
||||
site.siteId = m_forecastSiteId;
|
||||
// CHAQUE PLAN SOMMÉ, avec ses corrections — rendu seulement avec une courbe. La courbe du
|
||||
// site n'annonce que l'intersection ; sans le détail, « un plan sans température » et
|
||||
// « aucun plan corrigé » se liraient pareil. Trié, pour qu'un instantané se compare.
|
||||
if (parPlan) {
|
||||
std::sort(lus.begin(), lus.end(), [](const ForecastPlanLu &a, const ForecastPlanLu &b) {
|
||||
return a.planThingId < b.planThingId;
|
||||
});
|
||||
*parPlan = lus;
|
||||
}
|
||||
return site;
|
||||
}
|
||||
|
||||
|
||||
@ -119,10 +119,19 @@ public:
|
||||
*/
|
||||
QStringList forecastSiteCandidates() const;
|
||||
|
||||
//! Un plan SOMMÉ, tel qu'il a été lu : son Thing et les corrections qu'il déclare.
|
||||
struct ForecastPlanLu {
|
||||
QString planThingId;
|
||||
QStringList corrections; //!< ordre canonique ; vide = conversion brute
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief La prévision COURANTE, lue DIRECTEMENT sur le canal — l'entrée, pas la trace.
|
||||
* \param[out] fault Code de motif, vide si tout va bien.
|
||||
* \param[out] faultParams Paramètres du motif.
|
||||
* \param[out] parPlan Si non nul : un élément par plan SOMMÉ, trié par identifiant. Rempli
|
||||
* seulement quand une courbe est rendue — vide sinon. C'est là que se lit une divergence
|
||||
* de corrections entre plans : la courbe du site n'en porte que l'intersection (§30.2).
|
||||
* \return La révision telle que le producteur la publie ; \c points vide si rien n'est
|
||||
* lisible. **Une absence, jamais une courbe de zéros.**
|
||||
*
|
||||
@ -133,7 +142,8 @@ public:
|
||||
* parce qu'elle est nette maintenant ; dans six semaines, le premier consommateur prendrait
|
||||
* \c GetPvForecast sans y penser.
|
||||
*/
|
||||
ForecastRevision forecastSnapshot(QString &fault, QVariantMap &faultParams) const;
|
||||
ForecastRevision forecastSnapshot(QString &fault, QVariantMap &faultParams,
|
||||
QList<ForecastPlanLu> *parPlan = nullptr) const;
|
||||
|
||||
/*!
|
||||
* \brief Construit l'arbitre central. Mêmes dépendances que \c SmartChargingManager amont,
|
||||
|
||||
@ -395,7 +395,9 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
||||
"With a curve come 'corrections' (physical corrections applied, canonical "
|
||||
"order: 'temperature' by the provider, 'ac-limit' when the site has an AC "
|
||||
"limit; empty = raw GTI x kWp x PR; when planes disagree, only the corrections "
|
||||
"ALL of them received), 'conversion' ('physics' - this engine has no other) and "
|
||||
"ALL of them received), 'perField' (one entry per summed plane: its thing 'id' "
|
||||
"and the 'corrections' it declared - where a divergence between planes is "
|
||||
"read), 'conversion' ('physics' - this engine has no other) and "
|
||||
"'acLimitW' when an AC limit is configured: the curve is the sum of the planes "
|
||||
"CLIPPED to it.";
|
||||
QVariantMap snapPoint;
|
||||
@ -407,6 +409,10 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
||||
returns.insert("o:model", enumValueName(String));
|
||||
returns.insert("o:forecastPoints", QVariantList() << snapPoint);
|
||||
returns.insert("o:corrections", QVariantList() << enumValueName(String));
|
||||
QVariantMap snapPlan;
|
||||
snapPlan.insert("id", enumValueName(Uuid)); // Thing plan
|
||||
snapPlan.insert("corrections", QVariantList() << enumValueName(String));
|
||||
returns.insert("o:perField", QVariantList() << snapPlan);
|
||||
returns.insert("o:conversion", enumValueName(String)); // physics, seule valeur émise
|
||||
returns.insert("o:acLimitW", enumValueName(Uint));
|
||||
returns.insert("o:fault", enumValueName(String));
|
||||
@ -1243,7 +1249,8 @@ JsonReply *NymeaEnergyJsonHandler::GetForecastSnapshot(const QVariantMap ¶ms
|
||||
return createReply(ret);
|
||||
QString fault;
|
||||
QVariantMap faultParams;
|
||||
const ForecastRevision rev = arb->forecastSnapshot(fault, faultParams);
|
||||
QList<EnergyArbitrator::ForecastPlanLu> parPlan;
|
||||
const ForecastRevision rev = arb->forecastSnapshot(fault, faultParams, &parPlan);
|
||||
if (!fault.isEmpty()) {
|
||||
ret.insert(QStringLiteral("fault"), fault);
|
||||
if (!faultParams.isEmpty())
|
||||
@ -1281,6 +1288,13 @@ JsonReply *NymeaEnergyJsonHandler::GetForecastSnapshot(const QVariantMap ¶ms
|
||||
// Ce qui a été fait à la courbe, publié AVEC elle (OPTIMIZER_PROTOCOL §5.2) : une liste vide
|
||||
// est une affirmation — conversion brute — pas une absence.
|
||||
ret.insert(QStringLiteral("corrections"), QVariant(rev.corrections).toList());
|
||||
// PAR PLAN, en DONNÉE : le `corrections` du site est l'intersection, et une divergence entre
|
||||
// plans se lit ici — pas dans un booléen qui résumerait ce que les données disent déjà.
|
||||
QVariantList plansLus;
|
||||
for (const EnergyArbitrator::ForecastPlanLu &p : parPlan)
|
||||
plansLus.append(QVariantMap{{QStringLiteral("id"), p.planThingId},
|
||||
{QStringLiteral("corrections"), QVariant(p.corrections).toList()}});
|
||||
ret.insert(QStringLiteral("perField"), plansLus);
|
||||
// `physics` : le moteur GPL n'a pas d'autre conversion. Jamais `hybrid` (correction apprise,
|
||||
// hors GPL, §5). Une prévision dégradée se lit dans `corrections`, pas ici (§30.1).
|
||||
ret.insert(QStringLiteral("conversion"), QStringLiteral("physics"));
|
||||
|
||||
@ -517,6 +517,17 @@ QUuid EnergyTestBase::addMeterNoEnergy(quint16 port)
|
||||
return response.toMap().value("params").toMap().value("thingId").toUuid();
|
||||
}
|
||||
|
||||
QUuid EnergyTestBase::addPvPlane(const QString &nom)
|
||||
{
|
||||
QVariantMap params;
|
||||
params.insert("thingClassId", mockPvPlaneThingClassId.toString());
|
||||
params.insert("name", nom);
|
||||
|
||||
QVariant response = injectAndWait("Integrations.AddThing", params);
|
||||
verifyThingError(response);
|
||||
return response.toMap().value("params").toMap().value("thingId").toUuid();
|
||||
}
|
||||
|
||||
QUuid EnergyTestBase::addThermalProbe(quint16 port)
|
||||
{
|
||||
QVariantMap portParam;
|
||||
|
||||
@ -59,6 +59,7 @@ static QUuid mockPowerSwitchThingClassId = QUuid("841f8905-d1d7-4053-909f-01123b
|
||||
//! 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");
|
||||
static QUuid mockPvPlaneThingClassId = QUuid("4e9b2f71-8c3a-4d56-a1e0-7f2b9c6d3e18");
|
||||
static QUuid mockMeterNoEnergyThingClassId = QUuid("50dacc7b-7142-4716-8f59-97d6ee81f39c");
|
||||
static QUuid mockThermalProbeThingClassId = QUuid("37e61ea3-e4b2-4d3e-bc82-66e350c29f66");
|
||||
|
||||
@ -141,6 +142,14 @@ public:
|
||||
QUuid addThermalProbe(quint16 port = 26681);
|
||||
//! Mock thing implémentant l'interface etmvariableload (states currentPowerW / powerSetpoint).
|
||||
QUuid addEtmVariableLoad(quint16 port = 27001);
|
||||
/*!
|
||||
* \brief Ajoute un PLAN PV : un Thing qui porte l'état \c forecastCurveTransport.
|
||||
* \param nom Nom du Thing.
|
||||
* \return L'identifiant du Thing créé.
|
||||
* \note Le plan naît SANS parent : le test le range sous un site (\c Thing::setParentId) et
|
||||
* écrit sa charge utile. Sans lui, aucune courbe de prévision n'était productible.
|
||||
*/
|
||||
QUuid addPvPlane(const QString &nom = QStringLiteral("PV plane"));
|
||||
|
||||
void removeDevices();
|
||||
QVariant removeDevice(const QUuid &thingId);
|
||||
|
||||
@ -61,6 +61,7 @@ using namespace nymeaserver;
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <QHash>
|
||||
#include <QScopeGuard>
|
||||
#include <QtMath>
|
||||
#include <QtGlobal>
|
||||
#include <QProcess>
|
||||
@ -7211,6 +7212,111 @@ void Simulation::testTheSiteAcLimitTravelsWithTheDesignation()
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testTheSnapshotPublishesEachPlanesCorrections()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
// Décision du 2026-10-13 (§30.2) — les corrections PAR PLAN, en donnée. Le `corrections` du
|
||||
// site est l'intersection : sans le détail, « un plan sans température » et « aucun plan
|
||||
// corrigé » se liraient pareil. Pas de booléen « divergent » : la divergence se LIT.
|
||||
//
|
||||
// Premier test qui fait passer une COURBE par le RPC : le mock porte désormais une classe
|
||||
// `pvPlane` (canal `forecastCurveTransport`), rangée ici sous un site par `setParentId`.
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
QVERIFY(arb);
|
||||
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||||
const QUuid site = addMeter(); // n'importe quel Thing configuré tient lieu de site
|
||||
const QUuid est = addPvPlane(QStringLiteral("Toit est"));
|
||||
const QUuid ouest = addPvPlane(QStringLiteral("Toit ouest"));
|
||||
Thing *tEst = tm->findConfiguredThing(ThingId(est));
|
||||
Thing *tOuest = tm->findConfiguredThing(ThingId(ouest));
|
||||
QVERIFY(tEst && tOuest && !site.isNull());
|
||||
tEst->setParentId(ThingId(site));
|
||||
tOuest->setParentId(ThingId(site));
|
||||
// Détacher les plans quoi qu'il arrive : un enfant restant ferait échouer le nettoyage.
|
||||
const auto detacher = qScopeGuard([&]() {
|
||||
tEst->setParentId(ThingId());
|
||||
tOuest->setParentId(ThingId());
|
||||
arb->setForecastSiteId(QString());
|
||||
});
|
||||
// L'identifiant AVEC accolades, tel que `candidateSiteThingIds` le rend. ⚠️ Sans accolades,
|
||||
// la désignation est acceptée mais le snapshot reste muet : `forecastSnapshot()` compare des
|
||||
// CHAÎNES (défaut préexistant, signalé dans le rapport des décisions du 2026-10-13).
|
||||
const QString siteId = ThingId(site).toString();
|
||||
|
||||
const qint64 t0 = QDateTime(QDate(2026, 10, 13), QTime(10, 0), QTimeZone::utc()).toSecsSinceEpoch();
|
||||
auto charge = [&](const QVariantList &corrections, const QVariantList &p) {
|
||||
return QVariantMap{{"v", 2}, {"unit", "W"}, {"convention", "mean over [t, t+step)"},
|
||||
{"t0", t0}, {"step", 3600}, {"issuedAt", t0 - 3600},
|
||||
{"model", "meteofrance_seamless"}, {"p", p},
|
||||
{"corrections", corrections}};
|
||||
};
|
||||
auto snapshot = [&]() {
|
||||
return injectAndWait("NymeaEnergy.GetForecastSnapshot").toMap().value("params").toMap();
|
||||
};
|
||||
// `perField` rendu sous forme { id → corrections }, l'ordre étant vérifié à part.
|
||||
auto parPlan = [](const QVariantMap &snap) {
|
||||
QMap<QUuid, QStringList> m;
|
||||
for (const QVariant &e : snap.value("perField").toList())
|
||||
m.insert(e.toMap().value("id").toUuid(), e.toMap().value("corrections").toStringList());
|
||||
return m;
|
||||
};
|
||||
|
||||
// 1. Plans DIVERGENTS : l'un corrigé en température, l'autre brut. Plafond 2000 W.
|
||||
tEst->setStateValue("forecastCurveTransport", charge({"temperature"}, {1500, 1200}));
|
||||
tOuest->setStateValue("forecastCurveTransport", charge({}, {1200, 300}));
|
||||
QVariantMap r = injectAndWait("NymeaEnergy.SetForecastSite",
|
||||
{{"forecastSiteThingId", siteId}, {"acLimitW", 2000}}).toMap();
|
||||
QCOMPARE(r.value("params").toMap().value("energyError").toString(), QStringLiteral("EnergyErrorNoError"));
|
||||
QVariantMap snap = snapshot();
|
||||
QVERIFY2(snap.contains("forecastPoints"), qUtf8Printable(QJsonDocument::fromVariant(snap).toJson()));
|
||||
const QVariantList pts = snap.value("forecastPoints").toList();
|
||||
QCOMPARE(pts.count(), 2);
|
||||
QCOMPARE(pts.at(0).toMap().value("forecastWh").toDouble(), 2000.0); // 2700 écrêté
|
||||
QCOMPARE(pts.at(1).toMap().value("forecastWh").toDouble(), 1500.0);
|
||||
// Le site : l'intersection, plus le plafond. La température N'Y EST PAS — un plan ne l'a pas.
|
||||
QCOMPARE(snap.value("corrections").toStringList(), QStringList{"ac-limit"});
|
||||
QCOMPARE(snap.value("conversion").toString(), QStringLiteral("physics"));
|
||||
QCOMPARE(snap.value("acLimitW").toInt(), 2000);
|
||||
// Le détail : une entrée par plan sommé, et la divergence se lit dans les données.
|
||||
QMap<QUuid, QStringList> plans = parPlan(snap);
|
||||
QCOMPARE(plans.count(), 2);
|
||||
QCOMPARE(plans.value(est), QStringList{"temperature"});
|
||||
QCOMPARE(plans.value(ouest), QStringList{});
|
||||
// ac-limit est une correction du SITE : elle n'apparaît dans aucune entrée de plan.
|
||||
for (const QStringList &c : plans)
|
||||
QVERIFY(!c.contains("ac-limit"));
|
||||
// Trié par identifiant : un instantané se compare.
|
||||
const QVariantList brut = snap.value("perField").toList();
|
||||
QVERIFY(brut.at(0).toMap().value("id").toUuid().toString()
|
||||
< brut.at(1).toMap().value("id").toUuid().toString());
|
||||
|
||||
// 2. Plans CONCORDANTS, sans plafond : la température remonte au site, `ac-limit` disparaît,
|
||||
// et `acLimitW` est omis — jamais 0.
|
||||
tOuest->setStateValue("forecastCurveTransport", charge({"temperature"}, {1200, 300}));
|
||||
r = injectAndWait("NymeaEnergy.SetForecastSite", {{"forecastSiteThingId", siteId}}).toMap();
|
||||
QCOMPARE(r.value("params").toMap().value("energyError").toString(), QStringLiteral("EnergyErrorNoError"));
|
||||
snap = snapshot();
|
||||
QCOMPARE(snap.value("corrections").toStringList(), QStringList{"temperature"});
|
||||
QVERIFY(!snap.contains("acLimitW"));
|
||||
QCOMPARE(snap.value("forecastPoints").toList().at(0).toMap().value("forecastWh").toDouble(), 2700.0);
|
||||
plans = parPlan(snap);
|
||||
QCOMPARE(plans.value(est), QStringList{"temperature"});
|
||||
QCOMPARE(plans.value(ouest), QStringList{"temperature"});
|
||||
|
||||
// 3. Un plan REFUSÉ : ni courbe, ni détail par plan — le motif seul.
|
||||
tOuest->setStateValue("forecastCurveTransport", charge({"inconnue"}, {1200, 300}));
|
||||
snap = snapshot();
|
||||
QCOMPARE(snap.value("fault").toString(), QString::fromLatin1(DecisionCode::ForecastPayloadRefused));
|
||||
QVERIFY(!snap.contains("forecastPoints"));
|
||||
QVERIFY(!snap.contains("perField"));
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testEveryGetterReturnsWhatItDeclares()
|
||||
{
|
||||
// LE TROU QUE LE 2026-09-09 A OUVERT.
|
||||
|
||||
@ -213,6 +213,7 @@ private slots:
|
||||
void testSpotMarketIsOutOfTheBuild();
|
||||
//! Brief 3 T1 — acLimitW voyage avec la désignation de site, absent = aucun plafond.
|
||||
void testTheSiteAcLimitTravelsWithTheDesignation();
|
||||
void testTheSnapshotPublishesEachPlanesCorrections();
|
||||
//! TOUTE méthode Get* du namespace rend ce que sa déclaration promet.
|
||||
void testEveryGetterReturnsWhatItDeclares();
|
||||
//! Le snapshot sert la DÉCISION : le canal, jamais la table — et l'absence n'est pas zéro.
|
||||
|
||||
@ -439,6 +439,14 @@ void IntegrationPluginEnergyMocks::setupThing(ThingSetupInfo *info)
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
return;
|
||||
|
||||
} else if (thing->thingClassId() == pvPlaneThingClassId) {
|
||||
// PLAN PV — un canal `forecastCurveTransport`, rien d'autre. Le test le RANGE sous un
|
||||
// site (`Thing::setParentId`) et y écrit la charge utile lui-même : sans cette classe,
|
||||
// aucun test ne pouvait produire une courbe de prévision, et le snapshot n'était exercé
|
||||
// que dans ses cas vides.
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
return;
|
||||
|
||||
} else if (thing->thingClassId() == powerSwitchThingClassId) {
|
||||
EnergyMockController *controller = new EnergyMockController(thing, this);
|
||||
ParamType paramType = thing->thingClass().paramTypes().findByName("port");
|
||||
|
||||
@ -1211,8 +1211,28 @@
|
||||
"writable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pvPlane",
|
||||
"displayName": "Mocked PV plane (canal forecastCurveTransport)",
|
||||
"id": "4e9b2f71-8c3a-4d56-a1e0-7f2b9c6d3e18",
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"paramTypes": [
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "6a1c3e85-2d7f-4b90-9e14-0c8a5f3b7d22",
|
||||
"name": "forecastCurveTransport",
|
||||
"displayName": "Forecast curve — transport channel, not a value",
|
||||
"type": "QVariantMap",
|
||||
"defaultValue": {},
|
||||
"cached": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user