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.
127 lines
5.8 KiB
C++
127 lines
5.8 KiB
C++
// 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
|