From ac6dedbd536612ece788bc1e3da6575cc1196c4d Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Fri, 16 Oct 2026 08:01:00 +0200 Subject: [PATCH] =?UTF-8?q?refactor(pr=C3=A9vision):=20l'assemblage=20du?= =?UTF-8?q?=20snapshot=20passe=20du=20handler=20=C3=A0=20l'arbitre=20(brie?= =?UTF-8?q?f=204=20T0.5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- energyplugin/etm/energyarbitrator.cpp | 63 +++++++++++++++++++++++++ energyplugin/etm/energyarbitrator.h | 12 +++++ energyplugin/nymeaenergyjsonhandler.cpp | 62 ++---------------------- 3 files changed, 78 insertions(+), 59 deletions(-) diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp index a28d07f..bcddfa0 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -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 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> 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) diff --git a/energyplugin/etm/energyarbitrator.h b/energyplugin/etm/energyarbitrator.h index 90ed558..faec5e6 100644 --- a/energyplugin/etm/energyarbitrator.h +++ b/energyplugin/etm/energyarbitrator.h @@ -236,6 +236,18 @@ public: ForecastRevision forecastSnapshot(QString &fault, QVariantMap &faultParams, QList *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 »). diff --git a/energyplugin/nymeaenergyjsonhandler.cpp b/energyplugin/nymeaenergyjsonhandler.cpp index 8b43c2f..d789051 100644 --- a/energyplugin/nymeaenergyjsonhandler.cpp +++ b/energyplugin/nymeaenergyjsonhandler.cpp @@ -1353,66 +1353,10 @@ JsonReply *NymeaEnergyJsonHandler::GetForecastSnapshot(const QVariantMap ¶ms EnergyArbitrator *arb = dynamic_cast(m_smartChargingManager); if (!arb) return createReply(ret); - QString fault; - QVariantMap faultParams; - QList 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> 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 plafond = arb->forecastAcLimitW()) - ret.insert(QStringLiteral("acLimitW"), *plafond); #endif return createReply(ret); }