Patrick Schurig 1cfa9b68b1 feat: journaliser la divergence, et publier la frontière de période de reporting
DIVERGENCE JOURNALISÉE. `divergent` et `since` ne vivaient que dans la charge
utile de télémétrie et dans un QHash en mémoire — donc nulle part. Rien ne
comptait les épisodes : chaque redémarrage de nymead effaçait l'état, et le hash
est PURGÉ dès que l'écart cesse, si bien qu'une divergence qui s'ouvre et se
referme entre deux lectures ne laissait aucune trace. Un observateur externe
échantillonnant en continu aurait raté exactement les épisodes courts, qui sont
probablement les plus nombreux.

Conséquence : le critère « attendre que les traces parlent » attendait
indéfiniment. Le silence du journal était garanti par construction, pas par
l'absence du phénomène — l'instrument dont le silence ressemble à un résultat.
Deux `qCInfo`, aux deux transitions, avec la durée à la fermeture. Le journal de
.75 est persistant depuis le 2026-08-13 (200 Mo, un mois) : le comptage devient
trivial, et l'étape 4 pourra se décider sur des faits.

FRONTIÈRE DE PÉRIODE PUBLIÉE (LM-1005-b-iv). `System.GetTime` donne un NOM de
fuseau ; un décalage ne s'en déduit pas sans base de fuseaux, et embarquer une
base pour recalculer une frontière que le moteur connaît déjà resterait faux les
jours de changement d'heure, où la journée fait 23 ou 25 heures. Trois clients la
recalculeraient de trois façons, et la seule qui compte est celle qui a servi au
calcul. Même règle que `counts`.

C'est aussi la parade à la troisième nature d'heure murale : la période de
reporting suit l'installation, correctement, mais repose sur la prémisse que le
fuseau de la box SOIT celui de l'installation — et c'était le seul des trois cas
où personne ne vérifiait. `timeZone` rend la vérification possible.

DEUX CHAMPS, PAS UN (LM-1005-b-v), et c'est un ajout à la demande. La baseline se
repose à chaque redémarrage de nymead et à chaque recul de compteur, donc EN
PLEIN JOUR. Un jour où nymead a redémarré à 14 h, les ratios ne couvrent pas la
journée : ils couvrent depuis 14 h. Publier la seule frontière ferait écrire
« aujourd'hui » sur des chiffres qui ne parlent que de l'après-midi — un silence
de la même famille que celui qu'on ferme. `measuredSince` dit la portée réelle ;
quand les deux coïncident, le cas normal, il n'y a rien à dire de plus, et c'est
ce qui rend l'écart lisible le jour où il apparaît. Même paire, même raison que
`progress.measuredSince` / `targetWh`.

Les trois champs sont TOUJOURS présents, même quand les deux taux sont omis :
« la période a commencé à minuit et rien n'est encore mesurable » est une phrase
vraie et utile, là où une réponse vide ne se distingue pas d'une panne.

INVARIANT 11 au passage : le schéma du Get et celui du Changed étaient déclarés
DEUX fois. Ajouter trois champs à deux endroits est exactement la manière dont
ils divergent — d'où `energyRatiosSchema()`, déclaré une fois et servi aux deux,
comme `loadTelemetrySchema()`.

TEST vu mordre : remplacer `measuredSince` par la frontière — l'implémentation
naïve que la demande aurait pu produire — fait échouer l'assertion sur la repose
en plein jour (attendu 10:00, obtenu 00:00).

Suites : simulation 66/66, loadmodel 23/23, charging 48/48, spotmarket 32/32.
2026-09-05 08:01:16 +02:00

127 lines
5.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-energy-plugin-nymea.
*
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef ENERGYRATIOSCALCULATOR_H
#define ENERGYRATIOSCALCULATOR_H
#include <QDateTime>
/*!
* \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-là : premier appel,
* changement de jour local, OU compteur non monotone (Δ < 0 — redémarrage de nymead,
* thing ré-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; //!< 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 Début de la PÉRIODE de reporting en cours — le minuit local qui l'ouvre.
*
* Publié plutôt que laissé à déduire : \c System.GetTime donne un nom IANA, et un
* décalage ne s'en déduit pas sans base de fuseaux. Embarquer une base pour recalculer
* une frontière que le moteur connaît déjà serait le mauvais geste — et le recalcul
* resterait faux les jours de changement d'heure, où la journée fait 23 ou 25 heures.
*/
QDateTime debutPeriode;
/*!
* \brief Instant où la BASELINE courante a été posée. Ce n'est PAS toujours
* \c debutPeriode, et l'écart est la seule chose qui empêche les ratios de mentir.
*
* La baseline se repose à chaque redémarrage de nymead et à chaque recul de compteur.
* Un jour où nymead a redémarré à 14 h, les ratios ne couvrent PAS la journée : ils
* couvrent depuis 14 h. Publier la seule frontière de journée ferait afficher
* « aujourd'hui » sur des chiffres qui ne parlent que de l'après-midi.
*
* Même paire, même raison que \c measuredSince / \c targetWh de l'avancement éco.
*/
QDateTime mesureDepuis;
};
/*! \brief Construit sans baseline : le premier \c compute() la posera. */
EnergyRatiosCalculator() = default;
/*!
* \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();
/*!
* \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,
double totalAcquisition,
const QDateTime &now);
private:
static bool ratio(double numerator, double denominator, double *outPercent);
static qint64 localDay(const QDateTime &now);
bool m_hasBaseline = false;
//! Instant de pose de la baseline courante — publié en \c mesureDepuis.
QDateTime m_baselinePosee;
double m_baseProduction = 0.0;
double m_baseReturn = 0.0;
double m_baseConsumption = 0.0;
double m_baseAcquisition = 0.0;
qint64 m_baseDay = 0;
};
#endif // ENERGYRATIOSCALCULATOR_H