DÉFAUT DE CLASSE, trouvé par l'audit demandé après la bascule de fuseau. Quatre instants étaient stockés depuis QDateTime::currentDateTime(), donc en spécification Qt::LocalTime : un tel objet ne garde que des composantes d'HORLOGE MURALE et résout son époque au moment de la CONVERSION, avec le fuseau courant. m_baselinePosee → GetEnergyRatios.measuredSince m_ecoMesureDepuis → progress.measuredSince m_divergenceDepuis → command.since m_lastTelemetryPublish → alimente le précédent Mesuré sur la box le 2026-09-06, bascule Paris→London sans redémarrage : measuredSince 1788645607 → 1788649207, +3600 s. Le champ posé pour rendre une couture lisible était lui-même déplacé par la couture. ET `.toUTC()` AU RENDU NE PROTÉGEAIT DE RIEN — c'est le piège, parce que les quatre rendus l'appelaient déjà et avaient l'air corrects. Démontré par un banc Qt avec bascule de /etc/localtime sous un objet déjà stocké : AVANT stocké-local → 1788755728 stocké-UTC → 1788755728 (Europe/Paris) APRÈS stocké-local → 1788759328 stocké-UTC → 1788755728 (Europe/London) Correctif : `.toUTC()` À LA CAPTURE. periodStartedAt reste construit dans le fuseau courant — c'est une frontière locale, elle DOIT bouger. Les deux champs n'ont pas la même nature et le stockage le reflète maintenant. TEST avec son oracle : l'ancrage tient sous re-rendu, la contre-épreuve reproduit la dérive de +3600 s, et une quatrième assertion dit ce qui compte — le même décalage est un DÉFAUT sur l'instant de pose et le COMPORTEMENT ATTENDU sur la frontière. Le nombre ne dit pas lequel on regarde ; seule la nature du champ le dit. SIXIÈME FORME D'HOMOGÉNÉITÉ (AGENTS.md) : ce n'est plus le mock, c'est L'ENVIRONNEMENT DE VÉRIFICATION. Box et téléphone en Europe/Paris pendant toute la campagne : à décalage nul, un instant rendu en UTC, en heure box ou en heure téléphone donne le MÊME TEXTE. Aucun défaut de conversion ne pouvait se voir, et rien dans le code ne pouvait le signaler — la prémisse fausse n'est pas dans le banc, elle est dans la coïncidence de deux réglages extérieurs. D'où le geste : basculer le fuseau de temps en temps est un RÉVÉLATEUR, pas seulement une manipulation de capture. Quinze minutes en Europe/London ont dénoncé deux défauts, dont un qui avait traversé toute la campagne. Suites : loadmodel 25/25 (+1), simulation 67/67, charging 48/48, spotmarket 32/32. NON DÉPLOYÉ.
113 lines
4.9 KiB
C++
113 lines
4.9 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/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "energyratioscalculator.h"
|
|
|
|
#include <QTimeZone>
|
|
|
|
#include <algorithm>
|
|
|
|
void EnergyRatiosCalculator::reset()
|
|
{
|
|
m_hasBaseline = false;
|
|
m_baseProduction = 0.0;
|
|
m_baseReturn = 0.0;
|
|
m_baseConsumption = 0.0;
|
|
m_baseAcquisition = 0.0;
|
|
m_baseDay = 0;
|
|
// Sans cette ligne, un `mesureDepuis` périmé survivrait au reset et daterait la mesure
|
|
// d'avant l'installation qu'on vient de changer.
|
|
m_baselinePosee = QDateTime();
|
|
}
|
|
|
|
EnergyRatiosCalculator::Ratios EnergyRatiosCalculator::compute(double totalProduction,
|
|
double totalReturn,
|
|
double totalConsumption,
|
|
double totalAcquisition,
|
|
const QDateTime &now)
|
|
{
|
|
const qint64 day = localDay(now);
|
|
|
|
// Reseed si : 1er appel, nouveau jour local, OU compteur non monotone
|
|
// (Δ<0 → restart nymead / re-add thing / rollover). Jamais de ratio négatif.
|
|
const bool nonMonotone = m_hasBaseline &&
|
|
(totalProduction < m_baseProduction ||
|
|
totalReturn < m_baseReturn ||
|
|
totalConsumption < m_baseConsumption ||
|
|
totalAcquisition < m_baseAcquisition);
|
|
|
|
if (!m_hasBaseline || m_baseDay != day || nonMonotone) {
|
|
m_hasBaseline = true;
|
|
m_baseDay = day;
|
|
m_baseProduction = totalProduction;
|
|
m_baseReturn = totalReturn;
|
|
m_baseConsumption = totalConsumption;
|
|
m_baseAcquisition = totalAcquisition;
|
|
// L'instant de pose, distinct de la frontière de journée : un redémarrage de nymead
|
|
// repose la baseline en plein jour, et les ratios ne couvrent alors que depuis là.
|
|
//
|
|
// ANCRÉ EN UTC À LA CAPTURE, et c'est indispensable. Un QDateTime en spécification
|
|
// `Qt::LocalTime` ne garde que des composantes d'HORLOGE MURALE : son époque se résout
|
|
// au moment de la CONVERSION, avec le fuseau courant. Stocké tel quel, cet instant
|
|
// se déplaçait d'une heure au changement de fuseau — mesuré le 2026-09-06, bascule
|
|
// Paris→London : 1788645607 → 1788649207. Le champ posé pour rendre une couture
|
|
// lisible était lui-même déplacé par la couture.
|
|
//
|
|
// Un `.toUTC()` AU RENDU n'y aurait rien changé : il interprète les composantes murales
|
|
// avec le fuseau du moment. Seul l'ancrage à la capture tient.
|
|
m_baselinePosee = now.toUTC();
|
|
}
|
|
|
|
const double dProduction = totalProduction - m_baseProduction;
|
|
const double dReturn = totalReturn - m_baseReturn;
|
|
const double dConsumption = totalConsumption - m_baseConsumption;
|
|
const double dAcquisition = totalAcquisition - m_baseAcquisition;
|
|
|
|
Ratios result;
|
|
result.autoconsommationValid = ratio(dProduction - dReturn, dProduction, &result.autoconsommation);
|
|
result.autonomieValid = ratio(dConsumption - dAcquisition, dConsumption, &result.autonomie);
|
|
// La frontière se construit dans le fuseau de `now` — le même que celui qui a servi à
|
|
// décider du reseed. En prendre un autre ferait publier une frontière que le calcul
|
|
// n'a pas utilisée.
|
|
result.debutPeriode = QDateTime(now.date(), QTime(0, 0), now.timeZone());
|
|
result.mesureDepuis = m_baselinePosee;
|
|
return result;
|
|
}
|
|
|
|
// Ratio en % borné [0, 100]. Dénominateur ≤ 0 (nuit sans prod, etc.) → n/a
|
|
// (retourne false) — jamais de NaN.
|
|
bool EnergyRatiosCalculator::ratio(double numerator, double denominator, double *outPercent)
|
|
{
|
|
if (denominator <= 0)
|
|
return false;
|
|
*outPercent = std::clamp(numerator / denominator * 100.0, 0.0, 100.0);
|
|
return true;
|
|
}
|
|
|
|
qint64 EnergyRatiosCalculator::localDay(const QDateTime &now)
|
|
{
|
|
// Index de jour local stable (minuit local) — équivalent au _localDay interim.
|
|
return now.date().toJulianDay();
|
|
}
|