// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync #pragma once #include #include #include /*! * \file optimizerstatus.h * \brief Machine de repli §7 (OPTIMIZER_PROTOCOL.md 1.1) et ce qu'elle publie vers l'app * (\c NymeaEnergy.GetOptimizerStatus, brief 4 T4). * * Logique PURE : ni E/S, ni horloge (les instants sont fournis par l'appelant), ni accès à * l'arbitre. Le \c SocketScheduler y verse les événements de sa connexion ; l'arbitre compose * l'état publié (attendu, vivant, stratégie, mode dégradé L2). */ namespace OptimizerStatus { //! \c activeStrategy : le créneau courant d'un plan d'optimiseur a commandé le dernier cycle. constexpr const char *StrategieOptimiseur = "optimizer"; //! \c activeStrategy : le rule-based a commandé le dernier cycle (seul, ou en repli). constexpr const char *StrategieRegles = "rules"; //! \c activeStrategy : planification suspendue — mode dégradé L2, prioritaire sur tout (§7). constexpr const char *StrategieAucune = "none"; //! Requêtes consécutives en échec au-delà desquelles l'optimiseur est déclaré mort (§4, §7, E17). constexpr int EchecsAvantMort = 2; /*! * \brief Vivacité de l'optimiseur — la colonne \c optimizerAlive du tableau §7. * * | Événement | \c vivant() | échecs consécutifs | * |---|---|---| * | plan valide reçu | vrai | remis à 0 | * | abstention, plan expiré à la réception | inchangé | remis à 0 | * | requête en échec (délai, erreur, réponse illisible, plan invalide) | faux au 2ᵉ | +1 | * | connexion absente ou fermée | faux | inchangé | * * Les échecs se comptent par REQUÊTE, pas par cycle (écart E17) : une requête remplacée par une * plus récente avant sa réponse n'est pas un échec (le client n'émet rien, brief 4 T2). * \invariant Seul un plan valide rend la vie : une abstention dit que l'optimiseur répond, pas * qu'il planifie, et ne ressuscite donc pas un optimiseur déclaré mort. */ class Vivacite { public: /*! \brief Ce qui arrive à la connexion ou à une requête. */ enum class Evenement { PlanValide, //!< Réponse \c GetPlan jugée valide. Abstention, //!< Abstention, plan vide, aucun créneau courant, ou plan expiré à la réception. Echec, //!< Requête sans réponse utilisable, ou plan invalide. ConnexionPerdue //!< Socket absent, fermé, ou handshake refusé. }; /*! * \brief Applique un événement. * \param e Événement. * \param code Code d'erreur retenu comme \c lastErrorCode (\c Echec, \c ConnexionPerdue) ; * vide = inchangé. * \param instant Instant du plan valide (\c lastPlanAt) ; ignoré pour les autres événements. * \return Vrai si \c vivant() a changé. */ bool noter(Evenement e, const QString &code = QString(), const QDateTime &instant = QDateTime()); /*! \brief \return \c optimizerAlive. Faux tant qu'aucun plan valide n'a été reçu. */ bool vivant() const { return m_vivant; } /*! \brief \return Requêtes consécutives en échec depuis la dernière réponse valide. */ int echecsConsecutifs() const { return m_echecs; } /*! \brief \return Dernier code d'erreur vu (requête, plan ou connexion) ; vide si aucun. */ QString dernierCodeErreur() const { return m_dernierCode; } /*! \brief \return Instant du cycle dont la requête a reçu le dernier plan valide ; invalide si aucun. */ QDateTime dernierPlanA() const { return m_dernierPlanA; } private: bool m_vivant = false; int m_echecs = 0; QString m_dernierCode; QDateTime m_dernierPlanA; }; /*! * \brief La colonne « stratégie active » du tableau §7, composée sans attendre le cycle suivant. * \param degrade Mode dégradé L2 en cours. \param vivant \c optimizerAlive. * \param strategieDuCycle Stratégie qui a commandé le dernier cycle planifié (\c optimizer, * \c rules, ou \c none si aucun cycle n'a été planifié depuis l'entrée en L2). * \return \c none sous L2, prioritaire sur tout ; \c rules si le dernier cycle était à * l'optimiseur mais qu'il n'est plus vivant (mort, connexion perdue, endpoint retiré) — son * plan en cache a été abandonné, le cycle suivant sera au rule-based, et publier * « optimizer » d'ici là décrirait un état qui n'existe plus ; sinon \p strategieDuCycle. */ QString strategieActive(bool degrade, bool vivant, const QString &strategieDuCycle); /*! * \brief \c capabilities du handshake réduites à ce que l'app peut afficher (mode installateur). * \param capabilities \c result.capabilities tel que reçu. * \return Les seules clés documentées au §3 — \c maxHorizonH, \c resolutionS (nombres finis), * \c kinds, \c usesForecast (listes de codes connus), \c reasonCodes (booléen). Toute autre clé * ou valeur est retirée : rien de ce qu'un optimiseur écrit librement n'atteint l'app. */ QVariantMap capacitesPubliees(const QVariantMap &capabilities); /*! * \brief Chaîne d'identité d'un optimiseur (\c name, \c protocolVersion) rendue sûre pour l'app. * \param texte Valeur reçue au handshake. \param max Longueur gardée. * \return Caractères de contrôle retirés, blancs resserrés, coupée à \p max ; vide si rien ne reste. */ QString identite(const QString &texte, int max); }