refactor(prévision): l'assemblage du snapshot passe du handler à l'arbitre (brief 4 T0.5)
EnergyArbitrator::instantanePrevision(now) rend la charge utile de NymeaEnergy.GetForecastSnapshot — courbe, contentChangedAt, corrections par plan, conversion, acLimitW, base et prix sur la grille, ou le motif qui les remplace. Le handler n'ajoute que capturedAt, qui date la réponse. Le sérialiseur de l'optimiseur (T1) lira la même fonction : un seul assemblage. Code déplacé tel quel ; seuls changent les accès membres directs (m_forecastStore, m_forecastAcLimitW, que les accesseurs rendaient tels quels) et une garde redondante (`!rev.points.isEmpty()`, déjà garantie par le retour anticipé). Neutre : référence de décisions IDENTIQUE ; suites inchangées (211/135/80/36+11, 0 échec) ; aucun nouveau gabarit de journal >= info ; les 15 réponses GetForecastSnapshot des six tests qui l'appellent sont identiques avant/après (trace ponctuelle non commitée, ThingId et horodatages muraux neutralisés). contentChangedAt n'est rendu dans AUCUN test : son déplacement n'est prouvé que par le texte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ab36143385
commit
ac6dedbd53
@ -2478,6 +2478,69 @@ ForecastRevision EnergyArbitrator::forecastSnapshot(QString &fault, QVariantMap
|
||||
return site;
|
||||
}
|
||||
|
||||
QVariantMap EnergyArbitrator::instantanePrevision(const QDateTime &now) const
|
||||
{
|
||||
QVariantMap ret;
|
||||
QString fault;
|
||||
QVariantMap faultParams;
|
||||
QList<ForecastPlanLu> parPlan;
|
||||
const ForecastRevision rev = forecastSnapshot(fault, faultParams, &parPlan);
|
||||
// LA GRILLE, LA BASE ET LE PRIX — quel que soit le sort de la courbe PV (brief 3 T4). Un
|
||||
// site non désigné ne doit pas faire disparaître un tarif saisi ; la grille dit alors
|
||||
// laquelle elle est (celle du producteur par défaut), et `forecastPoints` reste absent.
|
||||
const QVariantMap series = seriesConsoEtPrix(fault.isEmpty() ? rev : ForecastRevision(), now);
|
||||
for (auto it = series.constBegin(); it != series.constEnd(); ++it)
|
||||
ret.insert(it.key(), it.value());
|
||||
if (!fault.isEmpty()) {
|
||||
ret.insert(QStringLiteral("fault"), fault);
|
||||
if (!faultParams.isEmpty())
|
||||
ret.insert(QStringLiteral("faultParams"), faultParams);
|
||||
return ret; // et AUCUN forecastPoints : l'absence est le message
|
||||
}
|
||||
if (rev.points.isEmpty())
|
||||
return ret; // le canal se tait : ni courbe, ni motif
|
||||
if (rev.issuedAt.isValid())
|
||||
ret.insert(QStringLiteral("issuedAt"), rev.issuedAt.toUTC().toString(Qt::ISODate));
|
||||
if (!rev.model.isEmpty())
|
||||
ret.insert(QStringLiteral("model"), rev.model);
|
||||
|
||||
// DATER LE CONTENU, pas seulement la lecture. Sans cela, un producteur FIGÉ se lirait comme
|
||||
// une prévision stable — l'instrument dont le silence ressemble à un résultat, sous sa forme
|
||||
// la plus coûteuse puisque celui qui le lit décide d'ACHETER.
|
||||
//
|
||||
// La date vient du magasin, qui sait quand ce contenu est apparu, et elle SURVIT au
|
||||
// redémarrage. Le garder en mémoire l'aurait remise à zéro à chaque relance — c'est-à-dire
|
||||
// qu'un producteur figé aurait paru frais après le premier redémarrage venu.
|
||||
// La table n'est pas lue comme ENTRÉE : elle ne fournit pas la courbe, seulement sa date.
|
||||
if (m_forecastStore && m_forecastStore->isValid()) {
|
||||
const QList<QPair<QDateTime, double>> revs = m_forecastStore->revisions(rev.points.first().targetAt);
|
||||
for (const auto &r : revs)
|
||||
if (qAbs(r.second - rev.points.first().wh) <= 0.001)
|
||||
ret.insert(QStringLiteral("contentChangedAt"), r.first.toUTC().toString(Qt::ISODate));
|
||||
}
|
||||
QVariantList pts;
|
||||
for (const ForecastPoint &p : rev.points)
|
||||
pts.append(QVariantMap{{"targetAt", p.targetAt.toUTC().toString(Qt::ISODate)},
|
||||
{"forecastWh", p.wh}});
|
||||
ret.insert(QStringLiteral("forecastPoints"), pts);
|
||||
// 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 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"));
|
||||
if (m_forecastAcLimitW)
|
||||
ret.insert(QStringLiteral("acLimitW"), *m_forecastAcLimitW);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void EnergyArbitrator::lireCourbePrevision()
|
||||
{
|
||||
if (!m_forecastStore || !m_forecastStore->isValid() || !m_tm)
|
||||
|
||||
@ -236,6 +236,18 @@ public:
|
||||
ForecastRevision forecastSnapshot(QString &fault, QVariantMap &faultParams,
|
||||
QList<ForecastPlanLu> *parPlan = nullptr) const;
|
||||
|
||||
/*!
|
||||
* \brief L'instantané de prévision ASSEMBLÉ : courbe, `contentChangedAt`, corrections par
|
||||
* plan, conversion, plafond AC, base et prix sur la grille — ou le motif qui les remplace.
|
||||
* \param now Instant de la réponse (grille par défaut de la base et du prix, sans courbe).
|
||||
* \return La charge utile de \c NymeaEnergy.GetForecastSnapshot, hors \c capturedAt : celui-ci
|
||||
* date la RÉPONSE, il reste au handler.
|
||||
* \note UNE fonction pour la RPC et pour le sérialiseur de l'optimiseur (brief 4 T1). Deux
|
||||
* assemblages finiraient par diverger, et le premier champ perdu serait le plus coûteux :
|
||||
* `contentChangedAt`, sans lequel un producteur figé se lit comme une prévision stable.
|
||||
*/
|
||||
QVariantMap instantanePrevision(const QDateTime &now) const;
|
||||
|
||||
/*!
|
||||
* \brief Construit l'arbitre central. Mêmes dépendances que \c SmartChargingManager amont,
|
||||
* dont il hérite (justification : AGENTS.md, « 3b-iii »).
|
||||
|
||||
@ -1353,66 +1353,10 @@ JsonReply *NymeaEnergyJsonHandler::GetForecastSnapshot(const QVariantMap ¶ms
|
||||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_smartChargingManager);
|
||||
if (!arb)
|
||||
return createReply(ret);
|
||||
QString fault;
|
||||
QVariantMap faultParams;
|
||||
QList<EnergyArbitrator::ForecastPlanLu> parPlan;
|
||||
const ForecastRevision rev = arb->forecastSnapshot(fault, faultParams, &parPlan);
|
||||
// LA GRILLE, LA BASE ET LE PRIX — quel que soit le sort de la courbe PV (brief 3 T4). Un
|
||||
// site non désigné ne doit pas faire disparaître un tarif saisi ; la grille dit alors
|
||||
// laquelle elle est (celle du producteur par défaut), et `forecastPoints` reste absent.
|
||||
const QVariantMap series = arb->seriesConsoEtPrix(fault.isEmpty() ? rev : ForecastRevision(),
|
||||
QDateTime::currentDateTimeUtc());
|
||||
for (auto it = series.constBegin(); it != series.constEnd(); ++it)
|
||||
// L'ASSEMBLAGE vit dans l'arbitre : le sérialiseur de l'optimiseur lit la même fonction.
|
||||
const QVariantMap snap = arb->instantanePrevision(QDateTime::currentDateTimeUtc());
|
||||
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it)
|
||||
ret.insert(it.key(), it.value());
|
||||
if (!fault.isEmpty()) {
|
||||
ret.insert(QStringLiteral("fault"), fault);
|
||||
if (!faultParams.isEmpty())
|
||||
ret.insert(QStringLiteral("faultParams"), faultParams);
|
||||
return createReply(ret); // et AUCUN forecastPoints : l'absence est le message
|
||||
}
|
||||
if (rev.points.isEmpty())
|
||||
return createReply(ret); // le canal se tait : ni courbe, ni motif
|
||||
if (rev.issuedAt.isValid())
|
||||
ret.insert(QStringLiteral("issuedAt"), rev.issuedAt.toUTC().toString(Qt::ISODate));
|
||||
if (!rev.model.isEmpty())
|
||||
ret.insert(QStringLiteral("model"), rev.model);
|
||||
|
||||
// DATER LE CONTENU, pas seulement la lecture. Sans cela, un producteur FIGÉ se lirait comme
|
||||
// une prévision stable — l'instrument dont le silence ressemble à un résultat, sous sa forme
|
||||
// la plus coûteuse puisque celui qui le lit décide d'ACHETER.
|
||||
//
|
||||
// La date vient du magasin, qui sait quand ce contenu est apparu, et elle SURVIT au
|
||||
// redémarrage. Le garder en mémoire l'aurait remise à zéro à chaque relance — c'est-à-dire
|
||||
// qu'un producteur figé aurait paru frais après le premier redémarrage venu.
|
||||
// La table n'est pas lue comme ENTRÉE : elle ne fournit pas la courbe, seulement sa date.
|
||||
if (ForecastStore *magasin = arb->forecastStore()) {
|
||||
if (magasin->isValid() && !rev.points.isEmpty()) {
|
||||
const QList<QPair<QDateTime, double>> revs = magasin->revisions(rev.points.first().targetAt);
|
||||
for (const auto &r : revs)
|
||||
if (qAbs(r.second - rev.points.first().wh) <= 0.001)
|
||||
ret.insert(QStringLiteral("contentChangedAt"), r.first.toUTC().toString(Qt::ISODate));
|
||||
}
|
||||
}
|
||||
QVariantList pts;
|
||||
for (const ForecastPoint &p : rev.points)
|
||||
pts.append(QVariantMap{{"targetAt", p.targetAt.toUTC().toString(Qt::ISODate)},
|
||||
{"forecastWh", p.wh}});
|
||||
ret.insert(QStringLiteral("forecastPoints"), pts);
|
||||
// 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"));
|
||||
if (const std::optional<int> plafond = arb->forecastAcLimitW())
|
||||
ret.insert(QStringLiteral("acLimitW"), *plafond);
|
||||
#endif
|
||||
return createReply(ret);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user