// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync #include "publishedplan.h" #include #include namespace PublishedPlan { namespace { QString iso(const QDateTime &t) { return t.toUTC().toString(Qt::ISODate); } QString nomKind(LoadAction::Kind k) { switch (k) { case LoadAction::Setpoint: return QStringLiteral("setpoint"); case LoadAction::State: return QStringLiteral("state"); case LoadAction::Constraint: return QStringLiteral("constraint"); } return QString(); } // Une action vers l'app : la charge, sa puissance, l'état SG-Ready, le motif EN CODE. Le motif // est celui de l'action (correctif a ; filtré à la réception pour un optimiseur, T3). QVariantMap entree(const LoadAction &a, double puissanceW, int etat) { QVariantMap m; m.insert(QStringLiteral("loadId"), a.loadId); m.insert(QStringLiteral("kind"), nomKind(a.kind)); m.insert(QStringLiteral("powerW"), puissanceW); if (a.kind == LoadAction::State) m.insert(QStringLiteral("state"), etat); m.insert(QStringLiteral("reasonCode"), a.reason.code); if (!a.reason.params.isEmpty()) m.insert(QStringLiteral("reasonParams"), a.reason.params); return m; } // Le créneau courant : ce que les adaptateurs ont APPLIQUÉ. Une charge délestée par L4 ou non // dispatchée n'a pas de retour d'applyAction : son action du créneau (fusionnée) fait foi. QVariantList actionsAppliquees(const Slot &slot, const QHash &appliquees) { QVariantList l; for (const LoadAction &a : slot.actions) { const auto r = appliquees.constFind(a.loadId); if (r != appliquees.constEnd()) l.append(entree(a, qMax(0.0, r->estimatedPowerW), r->state)); else l.append(entree(a, qMax(0.0, a.estimatedPowerW), a.state)); } return l; } QVariantMap creneau(const QDateTime &from, const QDateTime &to, const QVariantList &actions, const QVariantMap &expected) { QVariantMap c; c.insert(QStringLiteral("from"), iso(from)); c.insert(QStringLiteral("to"), iso(to)); c.insert(QStringLiteral("actions"), actions); const QVariantMap e = attenduPublie(expected); if (!e.isEmpty()) c.insert(QStringLiteral("expected"), e); return c; } } // namespace QVariantMap attenduPublie(const QVariantMap &expected) { QVariantMap out; for (const char *cle : {"gridImportW", "gridExportW"}) { const QVariant v = expected.value(QLatin1String(cle)); if (v.userType() != QMetaType::Double && v.userType() != QMetaType::Int && v.userType() != QMetaType::LongLong) continue; const double d = v.toDouble(); if (std::isfinite(d)) out.insert(QLatin1String(cle), d); } return out; } QVariantMap diagnosticsPublies(const QVariantMap &diagnostics) { static const QStringList statuts{QStringLiteral("optimal"), QStringLiteral("feasible"), QStringLiteral("best-so-far"), QStringLiteral("infeasible-relaxed")}; static const QStringList pv{QStringLiteral("physics"), QStringLiteral("hybrid"), QStringLiteral("none")}; QVariantMap out; const QVariant statut = diagnostics.value(QStringLiteral("status")); if (statut.userType() == QMetaType::QString && statuts.contains(statut.toString())) out.insert(QStringLiteral("status"), statut.toString()); const QVariant entrees = diagnostics.value(QStringLiteral("inputsUsed")); if (entrees.userType() == QMetaType::QVariantMap) { const QVariant p = entrees.toMap().value(QStringLiteral("pv")); if (p.userType() == QMetaType::QString && pv.contains(p.toString())) out.insert(QStringLiteral("inputsUsed"), QVariantMap{{QStringLiteral("pv"), p.toString()}}); } return out; } QVariantMap serialiser(const Plan &plan, const Slot &slot, const QHash &appliquees, const OptimizerPlan::PlanRecu *recu, const QString &codeRepli, const QDateTime &now) { QVariantMap m; m.insert(QStringLiteral("inForce"), true); const bool optimiseur = plan.strategy == QLatin1String(OptimizerPlan::StrategieOptimiseur) && recu && recu->indexCourant >= 0 && recu->indexCourant < recu->creneaux.size(); if (!optimiseur) { m.insert(QStringLiteral("strategy"), QLatin1String(StrategieRegles)); m.insert(QStringLiteral("computedAt"), iso(now)); if (slot.to.isValid()) m.insert(QStringLiteral("validUntil"), iso(slot.to)); m.insert(QStringLiteral("slots"), QVariantList{ creneau(slot.from.isValid() ? slot.from : now, slot.to.isValid() ? slot.to : now, actionsAppliquees(slot, appliquees), QVariantMap())}); if (!codeRepli.isEmpty()) m.insert(QStringLiteral("fallbackCode"), codeRepli); return m; } m.insert(QStringLiteral("strategy"), QLatin1String(OptimizerPlan::StrategieOptimiseur)); if (recu->computedAt.isValid()) m.insert(QStringLiteral("computedAt"), iso(recu->computedAt)); if (recu->validUntil.isValid()) m.insert(QStringLiteral("validUntil"), iso(recu->validUntil)); const OptimizerPlan::CreneauRecu &courant = recu->creneaux.at(recu->indexCourant); QVariantList creneaux{creneau(courant.from, courant.to, actionsAppliquees(slot, appliquees), courant.expected)}; // Les créneaux SUIVANTS : ce que l'optimiseur DEMANDE, informatif (§6) — rien n'y est encore // appliqué. Les passés ne se publient pas : ils ne sont plus en vigueur. for (int i = recu->indexCourant + 1; i < recu->creneaux.size(); ++i) { const OptimizerPlan::CreneauRecu &c = recu->creneaux.at(i); QVariantList actions; for (const OptimizerPlan::ActionRecue &ar : c.actions) actions.append(entree(ar.action, qMax(0.0, ar.action.estimatedPowerW), ar.action.state)); creneaux.append(creneau(c.from, c.to, actions, c.expected)); } m.insert(QStringLiteral("slots"), creneaux); const QVariantMap diag = diagnosticsPublies(recu->diagnostics); if (!diag.isEmpty()) m.insert(QStringLiteral("diagnostics"), diag); return m; } }