- OptimizerStatus::Vivacite : optimizerAlive, échecs comptés PAR REQUÊTE (E17), deux consécutifs = mort, socket fermé = mort, retour au premier plan valide ; une abstention rompt la série sans ressusciter. strategieActive() : none sous L2, prioritaire sur tout ; rules dès qu'un optimiseur n'est plus vivant, sans attendre le cycle suivant. - SocketScheduler : branchement de la vivacité, cache abandonné à la mort, UpdateLearned journalisé une fois et ignoré. - NymeaEnergy.GetOptimizerStatus + OptimizerStatusChanged (une notification par transition, charge utile déclarée une fois) ; champs installateur absents s'ils sont inconnus ; jamais le token, tokenConfigured seulement. Pas de tier (Q5). - SetOptimizerEndpoint (persisté, appliqué à chaud) et SetOptimizerToken (écriture seule, fichier 0600, Q2), tous deux PermissionScopeAdmin ; token déclaré Variant pour qu'un mauvais type ne soit pas recopié au journal par le validateur nymea. - NymeaEnergy.GetPlan : plan en vigueur (appliqué au créneau courant, demandé ensuite), codes et paramètres filtrés seulement, inForce toujours présent, fallbackCode quand un optimiseur configuré n'a pas commandé. - Tests : tableau §7 ligne par ligne, transitions notifiées une fois, RPC, token absent de tout journal du moteur et de toute réponse. Sans endpoint : decisions IDENTIQUE (23 792 lignes), suites inchangées, journal >= info inchangé (8 739 lignes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
5.3 KiB
C++
111 lines
5.3 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
|
#pragma once
|
|
|
|
#include <QDateTime>
|
|
#include <QString>
|
|
#include <QVariantMap>
|
|
|
|
/*!
|
|
* \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);
|
|
|
|
}
|