Régression contre une décision documentée : AGENTS.md, section « Verrous minOn/minOff », énonce que le scheduler clampe et décrémente le budget au palier réel. Le canal qui le permettait — minStage/maxStage dans LoadContextTelemetry — a été retiré en rév. 2/3. L'écrêtage a continué d'avoir lieu dans l'adaptateur, mais son résultat ne revenait plus à l'arbitre : pendant toute la fenêtre minOn, les charges de priorité suivante recevaient un résidu SURESTIMÉ et l'installation soutirait au réseau. Règle visée : la 4 — « bornes par adaptateur écrêtent TOUTE sortie de stratégie ». Pas la 1 : il n'y a qu'un décideur, le défaut est un décalage de comptabilité entre décision et exécution. Le canal est restauré EN WATTS, pas en index de palier : lockMinPowerW / lockMaxPowerW dans LoadContextTelemetry, remplis par RelayRouter::toLoadContext() depuis lockWindow(), convertis via la table de paliers. La frontière rév. 3 tient — aucun identifiant de relais ni index de combinaison ne remonte. Un lockMaxPowerW négatif signifie « aucun plafond », et non « plafond nul ». buildSetpointAction() applique ces bornes AVANT de décrémenter le budget, donc le résidu passé aux charges suivantes tient compte de la puissance engagée — quitte à devenir négatif, ce qui est la réponse correcte. Le decisionReason distingue ce cas : « Verrou minOn — X maintenue à N W (puissance engagée, budget M W) ; le résidu en tient compte ». Corriger dans le cycle, pas au cycle suivant : remonter le palier appliqué après coup n'aurait rattrapé l'erreur qu'au tour d'après. Test : testEcsBudgetUnderLock — deux charges classées, la première verrouillée à 2000 W sous un budget de 500 W ; la seconde reste à 0 parce que le résidu tient compte des 2000 W engagés. Build amd64 0 erreur. Simulation : 14/14. Réf. specs/spec_ecs.md §3 ECS-306 (0.5.1). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
4.4 KiB
C++
111 lines
4.4 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 <QtTest>
|
|
#include <QDateTime>
|
|
|
|
#include "simulationbase.h"
|
|
|
|
using namespace nymeaserver;
|
|
|
|
class ChargerPlugEvent
|
|
{
|
|
public:
|
|
ChargerPlugEvent() = default;
|
|
ChargerPlugEvent(const QDateTime &dt, bool pi = false, int pu = 0)
|
|
: dateTime{dt},
|
|
pluggedIn{pi},
|
|
percentageUsed{pu}
|
|
{}
|
|
QDateTime dateTime;
|
|
bool pluggedIn;
|
|
int percentageUsed = 0;
|
|
};
|
|
|
|
class Simulation: public SimulationBase
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
explicit Simulation(QObject *parent = nullptr) : SimulationBase(parent) { };
|
|
|
|
typedef QList<ChargerPlugEvent> ChargerPlugEvents;
|
|
typedef QList<int> DetailsStepList;
|
|
|
|
private slots:
|
|
void run_data();
|
|
void run();
|
|
|
|
// [T3] etmvariableload (ECS/routeur) : arrondi fixed (powerLevels) + clamp dynamic
|
|
// (maxPowerW), recrédit currentPowerW (anti-clignotement), délestage, round-trip powerSetpoint.
|
|
void testEcsSurplusPV();
|
|
|
|
// [T4] Watchdog L2 : compteur muet >90 s → etmvariableload setPowerSetpoint(0) force=true,
|
|
// planif suspendue (reste 0 sur N cycles), reprise au retour compteur.
|
|
void testMeterSilentFallback();
|
|
|
|
// [T4] Persistance LoadConfig (§4) : SetConfigs → fichier → relecture round-trip + validation.
|
|
void testLoadConfigPersistence();
|
|
// [T4] Construction des adaptateurs depuis la config : enabled servi, disabled exclu (§9).
|
|
void testLoadConfigBuildsAdapters();
|
|
// [T4] Injection RPC end-to-end : NymeaEnergy.Get/SetLoadConfig (couche consommée par l'app).
|
|
void testLoadConfigRpc();
|
|
// [rév.3] Chaîne complète : SetConfigs(relays[]) → rebuild → RelayRouter → cycle → commutation relais.
|
|
void testLoadConfigRelayRouter();
|
|
|
|
// SG-Ready (PAC) : montée d'états sur surplus, hystérésis 3↔4, protection court-cycling,
|
|
// + Volet 4 budget PARTAGÉ etmvariableload(ECS)↔PAC (waterfall unifié, inversion priorité).
|
|
void testSgReadySurplus();
|
|
|
|
// [rév.3] Combinatoire watts→relais DANS le RelayRouter : paliers dérivés, off-before-on,
|
|
// transition non-cascadée, et DÉDUPLICATION des niveaux (relais identiques fusionnés).
|
|
void testEcsRelayTopologies();
|
|
|
|
// [Phase 2] Ratios canoniques : EnergyRatiosCalculator aligné 1:1 sur le seam interim app
|
|
// (EnergyRatiosInterim.compute) — seed, normal, clamp, den≤0→n/a, non-monotone, nouveau jour.
|
|
void testEnergyRatiosAlignment();
|
|
|
|
// [étape 1 / ECS-306] Le budget décrémenté par le scheduler correspond au palier
|
|
// RÉELLEMENT applicable sous verrou minOn — les charges suivantes ne reçoivent pas un
|
|
// résidu surestimé.
|
|
void testEcsBudgetUnderLock();
|
|
// [étape 1 / ECS-411] Le palier courant est déduit de l'état réel des Things relais au
|
|
// démarrage, pas réinitialisé à 0.
|
|
void testEcsRestartRecovery();
|
|
// [étape 1 / ECS-412] SetLoadConfig pendant une fenêtre de verrou active ne réarme pas
|
|
// le verrou : seul le matériel modifié est reconstruit. Couvre aussi l'armement à froid.
|
|
void testEcsRebuildPreservesLock();
|
|
|
|
void printStates(Thing *thing);
|
|
void updateChargerMeter(Thing *thing);
|
|
|
|
QStringList plotOriginalData(int powerBalanceCount);
|
|
QStringList plotSimulation(const QString &title, int powerBalanceCount);
|
|
QStringList plotSpotMarketData(int powerBalanceCount);
|
|
|
|
};
|
|
|
|
Q_DECLARE_METATYPE(ChargerPlugEvent)
|
|
Q_DECLARE_METATYPE(Simulation::ChargerPlugEvents)
|
|
Q_DECLARE_METATYPE(Simulation::DetailsStepList)
|