feat(prévision PV): le cycle alimente l'enregistrement pour l'apprentissage
observerPourApprentissage(now), après lireCourbePrevision() : copies seulement (§11), aucune requête dans update(). Paramètres de conversion lus sur le Thing plan par leur nom, jamais recopiés (§28 décision 1). Minutes du bilan branchées sur EnergyLogs::powerBalanceEntryAdded(1 min), sous la garde de simulation. Mock : pvPlane porte kwp/performanceRatio/tempCoeffPctPerK/noctC ; nouvelle classe solarInverter (smartmeterproducer), seul producteur du mock. Test : testTheLearningLogIsFedByTheCycle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ff94377267
commit
2b83c206e4
@ -20,6 +20,7 @@
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include <energymanager.h>
|
||||
#include <energylogs.h>
|
||||
#include <QTimer>
|
||||
|
||||
namespace {
|
||||
@ -64,6 +65,9 @@ EnergyArbitrator::EnergyArbitrator(EnergyManager *em, ThingManager *tm,
|
||||
// Le magasin n'est PAS un déclencheur d'horloge murale : rien ne justifiait qu'il y soit.
|
||||
// Sa purge, si — elle reste dans la garde ci-dessous.
|
||||
m_forecastStore = new ForecastStore(ForecastStore::cheminParDefaut(), this);
|
||||
// L'ENREGISTREMENT POUR L'APPRENTISSAGE — même fichier, sa table, son fil : rien de ce qu'il
|
||||
// écrit ne passe par le cycle. Hors garde de simulation, pour la même raison que le magasin.
|
||||
m_forecastLearning = new ForecastLearningLog(ForecastStore::cheminParDefaut(), QTimeZone(), this);
|
||||
relireReglagesPrevision();
|
||||
|
||||
#ifndef ENERGY_SIMULATION
|
||||
@ -96,6 +100,16 @@ EnergyArbitrator::EnergyArbitrator(EnergyManager *em, ThingManager *tm,
|
||||
[this]() { m_forecastStore->purger(); });
|
||||
m_forecastPurge->start();
|
||||
}
|
||||
|
||||
// LA PRODUCTION MESURÉE, minute par minute, depuis le journal de l'amont : chaque
|
||||
// échantillon y est déjà une moyenne pondérée dans le temps sur sa minute. Déclencheur
|
||||
// d'horloge murale (le journal échantillonne sur un minuteur) : sous la garde, et le banc
|
||||
// d'essai injecte ses minutes par forecastLearningLog()->observerProduction().
|
||||
connect(em->logs(), &EnergyLogs::powerBalanceEntryAdded, this,
|
||||
[this](EnergyLogs::SampleRate rate, const PowerBalanceLogEntry &entry) {
|
||||
if (rate == EnergyLogs::SampleRate1Min && m_forecastLearning)
|
||||
m_forecastLearning->observerProduction(entry.timestamp(), entry.production());
|
||||
});
|
||||
#else
|
||||
Q_UNUSED(em)
|
||||
#endif
|
||||
@ -440,6 +454,7 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
||||
// vrai — un avertissement qui ne se déclencherait jamais.
|
||||
signalerSiteManquant();
|
||||
lireCourbePrevision();
|
||||
observerPourApprentissage(currentDateTime);
|
||||
|
||||
SurplusContext ctx = buildContext(currentDateTime);
|
||||
// Les dérogations vivent ENTRE la construction et la décision : elles se terminent sur ce
|
||||
@ -2518,6 +2533,62 @@ void EnergyArbitrator::lireCourbePrevision()
|
||||
m_forecastStore->enregistrer(site); // un run déjà connu rend faux, sans bruit
|
||||
}
|
||||
|
||||
void EnergyArbitrator::observerPourApprentissage(const QDateTime &now)
|
||||
{
|
||||
if (!m_forecastLearning)
|
||||
return;
|
||||
QList<ForecastPlanObservation> plans;
|
||||
int attendus = 0;
|
||||
std::optional<double> neige;
|
||||
bool producteur = false;
|
||||
if (!m_forecastSiteId.isEmpty() && m_tm) {
|
||||
// Un paramètre LU sur le Thing, par son NOM — paramètres puis réglages. Le producteur est
|
||||
// la seule source de la conversion (§28 décision 1) : on ne la recopie nulle part.
|
||||
auto parNom = [](Thing *t, const QString &nom) -> QVariant {
|
||||
const ParamType pt = t->thingClass().paramTypes().findByName(nom);
|
||||
if (pt.isValid())
|
||||
return t->paramValue(pt.id());
|
||||
const ParamType st = t->thingClass().settingsTypes().findByName(nom);
|
||||
return st.isValid() ? t->setting(st.id()) : QVariant();
|
||||
};
|
||||
for (Thing *thing : m_tm->configuredThings()) {
|
||||
if (thing->parentId().toString() != m_forecastSiteId
|
||||
|| !thing->thingClass().hasStateType("forecastCurveTransport"))
|
||||
continue;
|
||||
const QVariantMap charge = thing->stateValue("forecastCurveTransport").toMap();
|
||||
if (charge.isEmpty())
|
||||
continue; // rien publié : absent, comme pour la courbe
|
||||
++attendus; // refusé ou non, il fait partie du site
|
||||
QVariantMap parametres;
|
||||
for (const QString &nom : {QStringLiteral("kwp"), QStringLiteral("performanceRatio"),
|
||||
QStringLiteral("tempCoeffPctPerK"), QStringLiteral("noctC")}) {
|
||||
const QVariant v = parNom(thing, nom);
|
||||
if (v.isValid())
|
||||
parametres.insert(nom, v);
|
||||
}
|
||||
QString refus;
|
||||
ForecastPlanObservation obs = ForecastLearningLog::lirePlan(charge, parametres, refus);
|
||||
if (!refus.isEmpty())
|
||||
continue; // déjà journalisé par lireCourbePrevision()
|
||||
obs.planId = thing->id().toString();
|
||||
plans.append(obs);
|
||||
}
|
||||
// LA NEIGE QUI TOMBE, lue sur le site (état `snowfall`, une valeur de modèle) — pas la
|
||||
// neige qui COUVRE les panneaux, que rien ici ne mesure.
|
||||
if (Thing *site = m_tm->findConfiguredThing(ThingId(m_forecastSiteId))) {
|
||||
if (site->thingClass().hasStateType("snowfall")) {
|
||||
bool ok = false;
|
||||
const double v = site->stateValue("snowfall").toDouble(&ok);
|
||||
if (ok)
|
||||
neige = v;
|
||||
}
|
||||
}
|
||||
producteur = !m_tm->configuredThings().filterByInterface("smartmeterproducer").isEmpty();
|
||||
}
|
||||
m_forecastLearning->observer(now, m_forecastSiteId, plans, attendus, m_forecastAcLimitW,
|
||||
neige, producteur);
|
||||
}
|
||||
|
||||
void EnergyArbitrator::attacherFinsDerogation(QVariantMap &snap) const
|
||||
{
|
||||
// \rule{LM-1500-f} — FINS DE DÉROGATION, publiées au NIVEAU DE LA TRAME, pas dans loads[].
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
#include "types/drawcap.h"
|
||||
#include "types/derogation.h"
|
||||
#include "forecast/forecaststore.h"
|
||||
#include "forecast/forecastlearninglog.h"
|
||||
#include "types/decisionreason.h"
|
||||
#include "types/ecoprogress.h"
|
||||
#include "types/ecotarget.h"
|
||||
@ -79,6 +80,14 @@ public:
|
||||
*/
|
||||
ForecastStore *forecastStore() const { return m_forecastStore; }
|
||||
|
||||
/*!
|
||||
* \brief L'enregistrement pour l'apprentissage futur (brief 3 T2). \return Jamais nul.
|
||||
* \note Même fichier que \c forecastStore(), table à part, fil d'écriture à part. Exposé
|
||||
* pour le banc d'essai : c'est par lui qu'un test injecte les échantillons d'une minute
|
||||
* que le journal de l'amont fournit en production.
|
||||
*/
|
||||
ForecastLearningLog *forecastLearningLog() const { return m_forecastLearning; }
|
||||
|
||||
/*!
|
||||
* \brief Le SITE dont la prévision est retenue. \return ThingId, vide si aucun n'est désigné.
|
||||
* \note Une désignation GLOBALE, sur le modèle de \c Energy.SetRootMeter — même nature :
|
||||
@ -881,6 +890,14 @@ private:
|
||||
*/
|
||||
void lireCourbePrevision();
|
||||
|
||||
/*!
|
||||
* \brief Remet à l'enregistrement ce que les plans du site publient, et ce que le site lit.
|
||||
* \param now Temps du cycle — c'est lui qui dit quels pas sont écoulés.
|
||||
* \note COPIES seulement : rien ne revient vers le contexte ni le plan (§11). Aucune requête
|
||||
* n'est exécutée ici ; l'écriture se fait sur le fil de \c ForecastLearningLog.
|
||||
*/
|
||||
void observerPourApprentissage(const QDateTime &now);
|
||||
|
||||
|
||||
|
||||
/*!
|
||||
@ -952,6 +969,8 @@ private:
|
||||
QHash<QString, ILoadAdapter *> m_loadAdapters;
|
||||
//! Historique bitemporel des prévisions PV. Possédé ; purgé une fois par jour.
|
||||
ForecastStore *m_forecastStore = nullptr;
|
||||
//! Enregistrement pour l'apprentissage futur — même fichier, autre table, autre fil. Possédé.
|
||||
ForecastLearningLog *m_forecastLearning = nullptr;
|
||||
//! Dernier motif de refus vu par Thing — pour n'avertir qu'au CHANGEMENT de motif.
|
||||
QHash<QString, QString> m_forecastRefusVu;
|
||||
//! Site désigné ; vide = aucune prévision retenue, et c'est un cas NORMAL, jamais un défaut.
|
||||
|
||||
@ -528,6 +528,17 @@ QUuid EnergyTestBase::addPvPlane(const QString &nom)
|
||||
return response.toMap().value("params").toMap().value("thingId").toUuid();
|
||||
}
|
||||
|
||||
QUuid EnergyTestBase::addSolarInverter(const QString &nom)
|
||||
{
|
||||
QVariantMap params;
|
||||
params.insert("thingClassId", mockSolarInverterThingClassId.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;
|
||||
|
||||
@ -60,6 +60,7 @@ static QUuid mockPowerSwitchThingClassId = QUuid("841f8905-d1d7-4053-909f-01123b
|
||||
//! (docs/INVENTAIRE_MOCK.md).
|
||||
static QUuid mockDryRelayThingClassId = QUuid("5b2c7e41-9a06-4d3f-b8c1-6e0f2a7d4b39");
|
||||
static QUuid mockPvPlaneThingClassId = QUuid("4e9b2f71-8c3a-4d56-a1e0-7f2b9c6d3e18");
|
||||
static QUuid mockSolarInverterThingClassId = QUuid("7431ecd7-fc90-448e-b2bf-154948af786c");
|
||||
static QUuid mockMeterNoEnergyThingClassId = QUuid("50dacc7b-7142-4716-8f59-97d6ee81f39c");
|
||||
static QUuid mockThermalProbeThingClassId = QUuid("37e61ea3-e4b2-4d3e-bc82-66e350c29f66");
|
||||
|
||||
@ -150,6 +151,8 @@ public:
|
||||
* écrit sa charge utile. Sans lui, aucune courbe de prévision n'était productible.
|
||||
*/
|
||||
QUuid addPvPlane(const QString &nom = QStringLiteral("PV plane"));
|
||||
//! Un onduleur `smartmeterproducer` — la seule classe productrice du mock.
|
||||
QUuid addSolarInverter(const QString &nom = QStringLiteral("Onduleur"));
|
||||
|
||||
void removeDevices();
|
||||
QVariant removeDevice(const QUuid &thingId);
|
||||
|
||||
@ -69,6 +69,10 @@ using namespace nymeaserver;
|
||||
#include <QSignalSpy>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QCoreApplication>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlRecord>
|
||||
#include <QUuid>
|
||||
|
||||
#include <nymeacore.h>
|
||||
|
||||
@ -9068,3 +9072,154 @@ void Simulation::testLevelAbsenceAndZeroDifferInMeaning()
|
||||
QVERIFY2(vuUneCharge, "le mode dégradé doit publier les charges, sans leurs niveaux");
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testTheLearningLogIsFedByTheCycle()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
// Brief 3 T2, de bout en bout : un vrai cycle lit les plans d'un site, leurs PARAMÈTRES sur le
|
||||
// Thing (jamais recopiés), et remet une ligne par pas écoulé et par plan. Les unitaires de
|
||||
// loadmodel éprouvent les règles ; celui-ci éprouve le CÂBLAGE.
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
QVERIFY(arb);
|
||||
ForecastLearningLog *log = arb->forecastLearningLog();
|
||||
QVERIFY(log);
|
||||
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||||
const QUuid site = addMeter();
|
||||
const QUuid sud = addPvPlane(QStringLiteral("Toit sud"));
|
||||
const QUuid est = addPvPlane(QStringLiteral("Toit est"));
|
||||
Thing *tSud = tm->findConfiguredThing(ThingId(sud));
|
||||
Thing *tEst = tm->findConfiguredThing(ThingId(est));
|
||||
QVERIFY(tSud && tEst && !site.isNull());
|
||||
tSud->setParentId(ThingId(site));
|
||||
tEst->setParentId(ThingId(site));
|
||||
const auto detacher = qScopeGuard([&]() {
|
||||
tSud->setParentId(ThingId());
|
||||
tEst->setParentId(ThingId());
|
||||
arb->setForecastSiteId(QString());
|
||||
});
|
||||
tSud->setParamValue(QStringLiteral("kwp"), 2.0);
|
||||
tSud->setParamValue(QStringLiteral("performanceRatio"), 0.85);
|
||||
tEst->setParamValue(QStringLiteral("kwp"), 1.5); // porte aussi tempCoeff/noct (défauts)
|
||||
const QString siteId = ThingId(site).toString();
|
||||
QVERIFY(arb->setForecastSiteId(siteId, 5000));
|
||||
|
||||
// Grille ancrée sur l'HORLOGE MURALE + 3 jours : les révisions du site sont datées par
|
||||
// l'horloge murale (firstSeenAt), et « en vigueur à la veille / à H−1 » doit les trouver.
|
||||
QDateTime h = QDateTime::currentDateTimeUtc();
|
||||
h.setTime(QTime(h.time().hour(), 0));
|
||||
const QDateTime t0 = h.addDays(3);
|
||||
const qint64 t0s = t0.toSecsSinceEpoch();
|
||||
auto charge = [&](int v, const QVariantList &p) {
|
||||
QVariantMap c{{"v", v}, {"unit", "W"}, {"convention", "mean over [t, t+step)"},
|
||||
{"t0", t0s}, {"step", 3600}, {"issuedAt", t0s - 3 * 86400},
|
||||
{"model", "meteofrance_seamless"}, {"p", p}};
|
||||
if (v >= 2) {
|
||||
c["corrections"] = QVariantList{"temperature"};
|
||||
c["gti"] = QVariantList{700, 560, 420};
|
||||
c["tempC"] = QVariantList{18.5, 19.0, 19.5};
|
||||
}
|
||||
return c;
|
||||
};
|
||||
tSud->setStateValue("forecastCurveTransport", charge(2, {1500, 1200, 900}));
|
||||
tEst->setStateValue("forecastCurveTransport", charge(1, {700, 600, 500}));
|
||||
|
||||
auto cycle = [&](const QDateTime &n) {
|
||||
arb->simulationCallUpdate(n);
|
||||
QCoreApplication::processEvents();
|
||||
};
|
||||
auto lignes = [&](qint64 cible) {
|
||||
QList<QVariantMap> sortie;
|
||||
const QString nom = QStringLiteral("lecture-%1").arg(QUuid::createUuid().toString());
|
||||
{
|
||||
QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), nom);
|
||||
db.setDatabaseName(ForecastStore::cheminParDefaut());
|
||||
if (db.open()) {
|
||||
QSqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT l.*, (SELECT contentId FROM forecastPv f "
|
||||
" WHERE f.targetAt = l.targetAt AND f.siteId = l.siteId "
|
||||
" ORDER BY lastSeenAt DESC LIMIT 1) AS courant "
|
||||
"FROM forecastLearning l WHERE siteId = ? AND targetAt = ? "
|
||||
"ORDER BY planId"));
|
||||
q.addBindValue(siteId);
|
||||
q.addBindValue(cible);
|
||||
if (q.exec()) {
|
||||
while (q.next()) {
|
||||
QVariantMap m;
|
||||
const QSqlRecord r = q.record();
|
||||
for (int i = 0; i < r.count(); ++i)
|
||||
m.insert(r.fieldName(i), q.value(i));
|
||||
sortie << m;
|
||||
}
|
||||
}
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
QSqlDatabase::removeDatabase(nom);
|
||||
return sortie;
|
||||
};
|
||||
|
||||
// 1. SANS PRODUCTEUR — les minutes arrivent, mais la « production » du bilan n'est alors
|
||||
// qu'un zéro de construction : NULL, et la ligne n'est pas valide.
|
||||
const int lots0 = log->lotsTraites();
|
||||
cycle(t0.addSecs(300));
|
||||
for (int m = 1; m <= 60; ++m)
|
||||
log->observerProduction(t0.addSecs(60 * m), -1100);
|
||||
cycle(t0.addSecs(3600 + 150));
|
||||
QTRY_COMPARE(log->lotsTraites(), lots0 + 1);
|
||||
QList<QVariantMap> l = lignes(t0s);
|
||||
QCOMPARE(l.count(), 2);
|
||||
const QVariantMap rSud = l.at(0).value("planId").toString() == ThingId(sud).toString() ? l.at(0) : l.at(1);
|
||||
const QVariantMap rEst = l.at(0).value("planId").toString() == ThingId(est).toString() ? l.at(0) : l.at(1);
|
||||
QCOMPARE(rSud.value("planId").toString(), ThingId(sud).toString());
|
||||
QCOMPARE(rEst.value("planId").toString(), ThingId(est).toString());
|
||||
// Paramètres LUS sur le Thing plan.
|
||||
QCOMPARE(rSud.value("kwp").toDouble(), 2.0);
|
||||
QCOMPARE(rSud.value("performanceRatio").toDouble(), 0.85);
|
||||
QCOMPARE(rSud.value("tempCoeffPctPerK").toDouble(), -0.37);
|
||||
QCOMPARE(rSud.value("noctC").toDouble(), 45.0);
|
||||
QCOMPARE(rSud.value("gti").toDouble(), 700.0);
|
||||
QCOMPARE(rSud.value("tempC").toDouble(), 18.5);
|
||||
QCOMPARE(rSud.value("pW").toDouble(), 1500.0);
|
||||
QCOMPARE(rSud.value("corrections").toString(), QStringLiteral("temperature"));
|
||||
// v1 : le Thing porte tempCoeff/noct, la charge utile ne les a pas appliqués — NULL.
|
||||
QCOMPARE(rEst.value("kwp").toDouble(), 1.5);
|
||||
QCOMPARE(rEst.value("pW").toDouble(), 700.0);
|
||||
for (const char *c : {"gti", "tempC", "tempCoeffPctPerK", "noctC"})
|
||||
QVERIFY2(rEst.value(c).isNull(), c);
|
||||
for (const QVariantMap &r : {rSud, rEst}) {
|
||||
QCOMPARE(r.value("acLimitW").toInt(), 5000);
|
||||
QCOMPARE(r.value("sitePlans").toInt(), 2);
|
||||
QCOMPARE(r.value("sitePlansExpected").toInt(), 2);
|
||||
QCOMPARE(r.value("measuredMinutes").toInt(), 60);
|
||||
QVERIFY(r.value("measuredW").isNull());
|
||||
QVERIFY(r.value("measuredSource").isNull());
|
||||
QVERIFY(r.value("curtailed").isNull());
|
||||
QVERIFY(r.value("snow").isNull()); // le compteur-site n'a pas de snowfall
|
||||
QCOMPARE(r.value("valid").toInt(), 0);
|
||||
// Les RENVOIS vers la révision du site écrite par le même cycle — sa clé de contenu.
|
||||
QVERIFY(!r.value("courant").toString().isEmpty());
|
||||
QCOMPARE(r.value("contentIdHourAhead").toString(), r.value("courant").toString());
|
||||
QCOMPARE(r.value("contentIdDayAhead").toString(), r.value("courant").toString());
|
||||
}
|
||||
|
||||
// 2. AVEC UN ONDULEUR — la même mesure devient une mesure : publiée, sourcée, valide.
|
||||
addSolarInverter();
|
||||
for (int m = 61; m <= 120; ++m)
|
||||
log->observerProduction(t0.addSecs(60 * m), -1100);
|
||||
cycle(t0.addSecs(7200 + 150));
|
||||
QTRY_COMPARE(log->lotsTraites(), lots0 + 2);
|
||||
l = lignes(t0s + 3600);
|
||||
QCOMPARE(l.count(), 2);
|
||||
for (const QVariantMap &r : l) {
|
||||
QCOMPARE(r.value("measuredW").toDouble(), 1100.0);
|
||||
QCOMPARE(r.value("measuredSource").toString(), QStringLiteral("power-balance-1min"));
|
||||
QCOMPARE(r.value("curtailed").toInt(), 0);
|
||||
QCOMPARE(r.value("valid").toInt(), 1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -216,6 +216,8 @@ private slots:
|
||||
void testTheSnapshotPublishesEachPlanesCorrections();
|
||||
void testTheForecastSiteIdIsMatchedWhateverItsBraces();
|
||||
void testAnUnreadableForecastDateIsAnError();
|
||||
//! Brief 3 T2 — le cycle alimente l'enregistrement : paramètres du Thing, v1/v2, producteur.
|
||||
void testTheLearningLogIsFedByTheCycle();
|
||||
//! 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.
|
||||
|
||||
@ -447,6 +447,13 @@ void IntegrationPluginEnergyMocks::setupThing(ThingSetupInfo *info)
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
return;
|
||||
|
||||
} else if (thing->thingClassId() == solarInverterThingClassId) {
|
||||
// ONDULEUR — la seule classe `smartmeterproducer` du mock. Sans elle, « le site a-t-il un
|
||||
// producteur ? » ne pouvait valoir que NON dans le banc d'essai, et la production
|
||||
// mesurée de l'enregistrement pour l'apprentissage n'y était jamais publiée.
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
return;
|
||||
|
||||
} else if (thing->thingClassId() == powerSwitchThingClassId) {
|
||||
EnergyMockController *controller = new EnergyMockController(thing, this);
|
||||
ParamType paramType = thing->thingClass().paramTypes().findByName("port");
|
||||
|
||||
@ -1220,6 +1220,34 @@
|
||||
"user"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "85dc9979-b806-442d-bacd-370c5fb53daf",
|
||||
"name": "kwp",
|
||||
"displayName": "Peak power (kWp)",
|
||||
"type": "double",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "7d8062fb-bb6d-4ee8-822f-e5b93e9887a9",
|
||||
"name": "performanceRatio",
|
||||
"displayName": "Performance ratio",
|
||||
"type": "double",
|
||||
"defaultValue": 1.0
|
||||
},
|
||||
{
|
||||
"id": "5b294522-7bcf-4518-821b-1bf279528c89",
|
||||
"name": "tempCoeffPctPerK",
|
||||
"displayName": "Temperature coefficient (%/K)",
|
||||
"type": "double",
|
||||
"defaultValue": -0.37
|
||||
},
|
||||
{
|
||||
"id": "ebf95b72-f9ac-4253-bd27-52f0a7ca5eff",
|
||||
"name": "noctC",
|
||||
"displayName": "NOCT (°C)",
|
||||
"type": "double",
|
||||
"defaultValue": 45
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
@ -1231,6 +1259,37 @@
|
||||
"cached": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "solarInverter",
|
||||
"displayName": "Mocked solar inverter (smartmeterproducer)",
|
||||
"id": "7431ecd7-fc90-448e-b2bf-154948af786c",
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"smartmeterproducer"
|
||||
],
|
||||
"paramTypes": [
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "de8df554-bffb-4a53-a411-6cbff7f0002b",
|
||||
"name": "currentPower",
|
||||
"displayName": "Current power",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "3016a60a-549a-4b40-a37e-8cd3a16a8a26",
|
||||
"name": "totalEnergyProduced",
|
||||
"displayName": "Total energy produced",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user