docs(etm): contrats Doxygen — 25 derniers éléments hors fichiers différés

Complète la couverture : 31 → 6 avertissements. Les 6 restants sont exactement
les deux fichiers différés jusqu'à l'étape 3 (relayrouter.h 3,
energyarbitrator.h 3, ce dernier étant la cible d'ECS-412).

energyratioscalculator.h traité EN PREMIER, pour la raison qui le distingue :
c'était le seul endroit où l'absence de documentation cachait une règle métier.
La règle était en fait écrite — mais en commentaires « // », invisibles à
doxygen, donc invérifiables. Conversion en blocs Doxygen SANS réécriture du
fond : les trois cas de reseed de la baseline (premier appel, changement de jour
local, compteur non monotone) et la garantie « dénominateur ≤ 0 → n/a, jamais de
NaN » deviennent des invariants opposables. Précisé au passage que le paramètre
`now` doit être en heure LOCALE — passer de l'UTC déplacerait la frontière de
journée.

Reste du lot : loadaction.h (quels champs sont significatifs selon le kind),
plan.h ([from, to[ et l'interdiction de retourner un plan invalide),
loaddescriptor.h, loadconfigstore.h (tolérance de chargement : un fichier absent
n'est pas une erreur, une entrée invalide est ignorée seule).

Build amd64 0 erreur. Simulation : 7/7, dont testEnergyRatiosAlignment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-08-08 11:47:19 +02:00
parent e70a60c180
commit 41d3a329e4
5 changed files with 71 additions and 35 deletions

View File

@ -24,9 +24,17 @@ class LoadConfigStore : public QObject
{
Q_OBJECT
public:
/*!
* \brief Construit le store et charge le fichier s'il existe.
* \param parent Propriétaire Qt.
* \note Un fichier absent, illisible ou au JSON invalide n'est pas une erreur :
* le store démarre à vide. Une ENTRÉE invalide est ignorée individuellement
* (tolérance ascendante) on ne perd pas les autres pour une seule.
*/
explicit LoadConfigStore(QObject *parent = nullptr);
/*! \brief Configs actuellement en mémoire (chargées au démarrage ou via setConfigs). */
/*! \brief Configs actuellement en mémoire (chargées au démarrage ou via setConfigs).
* \return Copie de l'ensemble courant ; toutes ont passé \c LoadConfig::isValid(). */
LoadConfigs configs() const { return m_configs; }
/*!

View File

@ -27,36 +27,59 @@
#include <QDateTime>
// [Phase 2] Ratios énergétiques canoniques (autoconsommation / autonomie), en %.
//
// Source canonique côté energymanager : remplace le seam interim app-side
// (EnergyRatiosInterim.compute). PUR MESURE (frontière GPL) : dérive par Δ de
// cumuls sur la période affichée (journée depuis minuit local), pas par
// intégration du signal live → déterministe, pas de drift de polling.
//
// autonomie = (Δ totalConsumption Δ totalAcquisition) / Δ totalConsumption
// autoconsommation = (Δ totalProduction Δ totalReturn) / Δ totalProduction
//
// Reseed de la baseline : 1er appel, nouveau jour local, OU compteur non
// monotone (Δ<0 : restart nymead / re-add thing / rollover). Dénominateur ≤ 0
// → n/a (valid=false), jamais de NaN. Porté 1:1 de l'interim app.
/*!
* \brief [Phase 2] Ratios énergétiques canoniques (autoconsommation / autonomie), en %.
*
* Source canonique côté energymanager : remplace le seam interim app-side
* (\c EnergyRatiosInterim.compute). **PUR MESURE** (frontière GPL) : dérive par Δ de
* cumuls sur la période affichée (journée depuis minuit local), pas par intégration du
* signal live déterministe, pas de drift de polling.
*
* \par Formules
* \code
* autonomie = (Δ totalConsumption Δ totalAcquisition) / Δ totalConsumption
* autoconsommation = (Δ totalProduction Δ totalReturn) / Δ totalProduction
* \endcode
*
* \invariant Reseed de la baseline dans trois cas, et seulement ceux- : premier appel,
* changement de jour local, OU compteur non monotone (Δ < 0 redémarrage de nymead,
* thing -ajouté, rollover).
* \invariant Dénominateur 0 résultat marqué n/a (\c valid == false). **Jamais de NaN**,
* jamais un 0 % qui se ferait passer pour une mesure.
* \invariant Porté 1:1 de l'interim app toute divergence de formule est un défaut.
*/
class EnergyRatiosCalculator
{
public:
/*! \brief Résultat d'un calcul : deux ratios, chacun accompagné de sa validité. */
struct Ratios {
bool autoconsommationValid = false; // false = n/a (dénominateur nul)
double autoconsommation = 0.0; // % dans [0, 100]
bool autonomieValid = false; // false = n/a
double autonomie = 0.0; // % dans [0, 100]
bool autoconsommationValid = false; //!< Faux = n/a (dénominateur nul) — ne pas afficher \c autoconsommation.
double autoconsommation = 0.0; //!< Part de la production consommée sur place, % dans [0, 100].
bool autonomieValid = false; //!< Faux = n/a (dénominateur nul) — ne pas afficher \c autonomie.
double autonomie = 0.0; //!< Part de la consommation couverte sans soutirage, % dans [0, 100].
};
/*! \brief Construit sans baseline : le premier \c compute() la posera. */
EnergyRatiosCalculator() = default;
// Réinitialise la baseline (ex. switch d'installation).
/*!
* \brief Réinitialise la baseline (ex. changement d'installation).
* \note Le \c compute() suivant repose la baseline et retourne donc n/a sur les deux
* ratios : sans écart de cumuls, il n'y a rien à mesurer.
*/
void reset();
// Calcule depuis les cumuls courants (mêmes unités en entrée — elles
// s'annulent dans le ratio). `now` (heure LOCALE) sert à détecter le jour.
/*!
* \brief Calcule les deux ratios depuis les cumuls courants.
* \param totalProduction Cumul de production. Unité libre elle s'annule dans le ratio.
* \param totalReturn Cumul réinjecté au réseau, même unité.
* \param totalConsumption Cumul de consommation, même unité.
* \param totalAcquisition Cumul soutiré au réseau, même unité.
* \param now Horodatage en heure **LOCALE** : sert à détecter le changement
* de jour, donc le reseed. Passer de l'UTC fausserait la frontière
* de journée.
* \return Les deux ratios ; un ratio dont le dénominateur est 0 revient \c valid == false.
*/
Ratios compute(double totalProduction,
double totalReturn,
double totalConsumption,

View File

@ -28,24 +28,24 @@ struct LoadAction {
enum Permission { Allow, Forbid };
QString loadId; //!< ThingId de la charge cible (string).
Kind kind = Setpoint;
Funding funding = Surplus;
Kind kind = Setpoint; //!< Détermine QUELS champs ci-dessous sont significatifs.
Funding funding = Surplus; //!< Origine du budget engagé. Interne — jamais sérialisé.
// --- Setpoint evcharger ---
bool chargingEnabled = false;
bool chargingEnabled = false; //!< Autorise la charge ; faux = suspendue (pas 0 A).
double currentA = 0; //!< Courant consigne (A), écrêté par l'adaptateur.
uint phaseCount = 0; //!< Nombre de phases (1 ou 3, 0 = inchangé).
// --- Setpoint battery ---
double powerW = 0;
Source source = Solar;
double powerW = 0; //!< Consigne en W. Pour une charge pilotée : l'ENVELOPPE allouée.
Source source = Solar; //!< Énergie autorisée pour cette consigne batterie.
// --- State sg-ready ---
int state = 0; //!< État SG-Ready (1-4).
// --- Constraint battery ---
Permission charge = Allow;
Permission discharge = Allow;
Permission charge = Allow; //!< Autorisation de charger la batterie sur ce créneau.
Permission discharge = Allow; //!< Autorisation de la décharger (arbitrage grid-funding).
/*!
* \brief Motif de la décision, non vide, en français.

View File

@ -84,9 +84,10 @@ struct LoadDescriptor {
QString label; //!< Nom lisible (affiché dans les logs).
//! Type d'adaptateur : "evcharger"|"etmvariableload"|"sg-ready"|"battery".
QString adapter;
//! Rang de service, ASCENDANT (1 = premier servi) — cf. la note de classe ci-dessus.
int priority = 0;
LoadDeclared declared;
LoadLimits limits;
LoadNeeds needs;
LoadDeclared declared; //!< Capacités de plaque : paliers, plafond, états.
LoadLimits limits; //!< Verrous temporels imposés par le matériel.
LoadNeeds needs; //!< Besoins utilisateur, transmis tels quels à l'optimiseur.
QList<LoadAction::Kind> supportedKinds; //!< Kinds acceptés par applyAction().
};

View File

@ -17,9 +17,9 @@
* \invariant Un Slot vide (actions vide) est valide signifie "aucune action ce créneau".
*/
struct Slot {
QDateTime from;
QDateTime to;
QList<LoadAction> actions;
QDateTime from; //!< Début du créneau, inclus.
QDateTime to; //!< Fin du créneau, EXCLUE — l'intervalle est [from, to[.
QList<LoadAction> actions; //!< Actions à appliquer, triées par priorité croissante.
};
/*!
@ -49,6 +49,10 @@ struct Plan {
return {};
}
/*! \brief Vrai si le plan contient au moins un Slot. */
/*!
* \brief Vrai si le plan contient au moins un Slot.
* \return Faux uniquement si \c timeSlots est vide. Un scheduler ne DOIT jamais
* retourner un plan invalide (invariant IScheduler) pas d'abstain vers l'arbitre.
*/
bool isValid() const { return !timeSlots.isEmpty(); }
};