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); }