[3c-3] waterfall ECS dans RuleBasedScheduler::getPlan() + tri priorité ASC
Corrections A (déduction EV unique) et B (anti-clignotement) intégrées. Tri priorité ascendant (rang 1 = premier servi, OPTIMIZER_PROTOCOL §5/annexe C) — corrige l'inversion du PLAN 3C et 3 doc-comments (plan.h, loaddescriptor.h, ecsrelayadapter.h). Build 0 erreur / 0 warning. telemetry() ECS : currentPowerW MESURÉE si au moins un relais expose "currentPower" (thermostat coupé → 0, pas de fantôme), DÉCLARÉE en repli seulement sans comptage. Dette evadapter.cpp priority=100 (ancienne convention) inscrite en 3g. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7709057335
commit
6298d5d42f
10
AGENTS.md
10
AGENTS.md
@ -72,7 +72,7 @@ Après la boucle EV proxy, ajouter :
|
||||
evReservedW = Σ EV en charge dans slot.actions : max(0, commandedA×phases×230 − ev->currentPower())
|
||||
remainingSurplusW = max(0, ctx.meter.exportW − evReservedW)
|
||||
|
||||
// Tri loads ECS par priorité DESC (200 = servi en premier)
|
||||
// Tri loads ECS par priorité ASC (priority 1 = servi en premier ; protocole §5 + annexe C)
|
||||
pour chaque LoadContext lc où lc.adapter == "relay-stages" :
|
||||
budgetCharge = remainingSurplusW + lc.telemetry.currentPowerW // correction B anti-clignotement
|
||||
bestStage = palier le plus haut dont stages[i] ≤ budgetCharge
|
||||
@ -238,6 +238,14 @@ au compteur).
|
||||
`RuleBasedScheduler` → priorités libres entre toutes les charges (EV, ECS, SG-Ready,
|
||||
batterie).
|
||||
|
||||
**Dette 3g — convention de priorité EV** : `EvAdapter::descriptor()` met
|
||||
`priority = 100` (`evadapter.cpp:24`), reliquat de l'ancienne convention « poids,
|
||||
valeur haute = premier ». Inoffensif en beta : l'EV est servi par le proxy *avant* le
|
||||
waterfall ECS et n'entre pas dans le tri ECS (ascendant, rang 1 = premier servi,
|
||||
protocole §5). À reconcilier quand l'EV rejoindra le waterfall unifié : la priorité
|
||||
devient un **rang** (1, 2, 3…), pas un poids — sinon `priority=100` placerait l'EV en
|
||||
dernier d'un tri ascendant.
|
||||
|
||||
---
|
||||
|
||||
### 3b-iii — EnergyArbitrator hérite de SmartChargingManager
|
||||
|
||||
@ -55,18 +55,29 @@ LoadTelemetry EcsRelayAdapter::telemetry() const
|
||||
t.available = true;
|
||||
t.lastActionAt = m_lastActionAt;
|
||||
|
||||
// Puissance mesurée = somme des relais actifs pour le stage courant
|
||||
// Source de currentPowerW (consommée par le recrédit anti-clignotement du waterfall,
|
||||
// RuleBasedScheduler::buildEcsStageAction — correction B) :
|
||||
// - MESURÉE dès qu'au moins un relais du stage expose un state "currentPower" :
|
||||
// somme des mesures. Un thermostat interne coupé (relais ON mais 0 W réel) renvoie
|
||||
// alors 0 → recrédit 0, jamais de puissance fantôme.
|
||||
// - DÉCLARÉE (repli) : stages[stage] UNIQUEMENT si aucun relais actif ne mesure
|
||||
// (relais nus / mock sans powermetering). Approximation = palier à sa nominale.
|
||||
double power = 0;
|
||||
bool metered = false;
|
||||
const QList<QString> &activeRelays = m_currentStage < m_relayMapping.size()
|
||||
? m_relayMapping.at(m_currentStage)
|
||||
: QList<QString>();
|
||||
for (const QString &thingId : activeRelays) {
|
||||
Thing *t2 = m_thingManager->findConfiguredThing(ThingId(thingId));
|
||||
if (t2)
|
||||
power += t2->stateValue("currentPower").toDouble();
|
||||
if (!t2)
|
||||
continue;
|
||||
if (!t2->thingClass().stateTypes().findByName("currentPower").id().isNull()) {
|
||||
metered = true;
|
||||
power += t2->stateValue("currentPower").toDouble();
|
||||
}
|
||||
}
|
||||
// Si pas de powermetering dans le mock, on estime depuis les stages déclarés
|
||||
if (power == 0 && m_currentStage > 0 && m_currentStage < m_stages.size())
|
||||
// Repli déclaré : seulement si la mesure n'est pas disponible (pas si elle vaut 0).
|
||||
if (!metered && m_currentStage > 0 && m_currentStage < m_stages.size())
|
||||
power = m_stages.at(m_currentStage);
|
||||
|
||||
t.currentPowerW = power;
|
||||
|
||||
@ -42,7 +42,8 @@ public:
|
||||
* \param relayMapping relayMapping[i] = liste de ThingIds powerswitch ON pour le palier i.
|
||||
* \param minOnS Durée minimale ON (s) — anti-rebond.
|
||||
* \param minOffS Durée minimale OFF (s) — anti-rebond.
|
||||
* \param priority Priorité dans le waterfall (200=deadline, 100=normal, 0=différable).
|
||||
* \param priority Rang dans le waterfall (OPTIMIZER_PROTOCOL §5) : valeur plus BASSE
|
||||
* = servi en premier (rang 1 = premier servi).
|
||||
* \param parent Propriétaire Qt.
|
||||
*/
|
||||
explicit EcsRelayAdapter(ThingManager *thingManager,
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
#include "plugininfo.h"
|
||||
#include <QUuid>
|
||||
#include <algorithm>
|
||||
|
||||
RuleBasedScheduler::RuleBasedScheduler(EnergyArbitrator *arbitrator, QObject *parent)
|
||||
: QObject(parent)
|
||||
@ -78,6 +79,35 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
|
||||
slot.actions.append(la);
|
||||
}
|
||||
|
||||
// ---- Waterfall ECS (charges non-EV à paliers) ---------------------------------
|
||||
// Correction A — déduction EV unique : ctx.meter.exportW est la mesure BRUTE
|
||||
// (invariant 8). Les consignes EV de CE cycle ne sont pas encore visibles au
|
||||
// compteur ; on réserve donc leur puissance commandée non encore mesurée.
|
||||
double evReservedW = 0;
|
||||
for (const LoadAction &la : slot.actions) {
|
||||
if (la.kind != LoadAction::Setpoint || !la.chargingEnabled)
|
||||
continue;
|
||||
EvCharger *ev = evs.value(ThingId(la.loadId));
|
||||
const double measuredW = ev ? ev->currentPower() : 0.0;
|
||||
evReservedW += qMax(0.0, la.estimatedPowerW - measuredW);
|
||||
}
|
||||
double remainingSurplusW = qMax(0.0, ctx.meter.exportW - evReservedW);
|
||||
|
||||
// Charges ECS triées par priorité ASCENDANTE : rang 1 = premier servi
|
||||
// (OPTIMIZER_PROTOCOL §5 + annexe C — la priorité est un rang, pas un poids).
|
||||
QList<LoadContext> ecsLoads;
|
||||
for (const LoadContext &lc : ctx.loads) {
|
||||
if (lc.adapter == QStringLiteral("relay-stages"))
|
||||
ecsLoads.append(lc);
|
||||
}
|
||||
std::sort(ecsLoads.begin(), ecsLoads.end(),
|
||||
[](const LoadContext &a, const LoadContext &b) { return a.priority < b.priority; });
|
||||
|
||||
for (const LoadContext &lc : ecsLoads)
|
||||
slot.actions.append(buildEcsStageAction(lc, remainingSurplusW));
|
||||
|
||||
// Grid funding ECS : dormant jusqu'à 3f (waterfall réseau) — non implémenté ici.
|
||||
|
||||
Plan plan;
|
||||
plan.planId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
plan.strategy = QStringLiteral("rule-based");
|
||||
@ -132,3 +162,39 @@ LoadAction RuleBasedScheduler::buildIdleAction(EvCharger *ev) const
|
||||
la.estimatedPowerW = 0;
|
||||
return la;
|
||||
}
|
||||
|
||||
LoadAction RuleBasedScheduler::buildEcsStageAction(const LoadContext &lc,
|
||||
double &remainingSurplusW) const
|
||||
{
|
||||
// Correction B (anti-clignotement) : on recrédite la conso actuelle de l'ECS au budget.
|
||||
// Sans ce recrédit, allumer un palier ferait chuter l'export mesuré → palier 0 → oscillation.
|
||||
const double budgetChargeW = remainingSurplusW + lc.telemetry.currentPowerW;
|
||||
|
||||
// Palier déclaré le plus haut qui tient dans le budget. stages[0] == 0 par construction
|
||||
// (Q_ASSERT EcsRelayAdapter) → bestStage ≥ 0 toujours, donc reason toujours définie.
|
||||
const QList<int> &stages = lc.declared.stages;
|
||||
int bestStage = 0;
|
||||
for (int i = 0; i < stages.size(); ++i) {
|
||||
if (stages.at(i) <= budgetChargeW)
|
||||
bestStage = i;
|
||||
}
|
||||
|
||||
LoadAction la;
|
||||
la.loadId = lc.id;
|
||||
la.kind = LoadAction::Stage;
|
||||
la.funding = LoadAction::Surplus;
|
||||
la.stage = bestStage;
|
||||
la.estimatedPowerW = bestStage < stages.size() ? stages.at(bestStage) : 0;
|
||||
|
||||
if (bestStage > 0)
|
||||
la.reason = QStringLiteral("Surplus PV %1 W — %2 palier %3 (%4 W)")
|
||||
.arg(qRound(budgetChargeW)).arg(lc.label)
|
||||
.arg(bestStage).arg(stages.at(bestStage));
|
||||
else
|
||||
la.reason = QStringLiteral("Surplus insuffisant (%1 W) — %2 éteint")
|
||||
.arg(qRound(budgetChargeW)).arg(lc.label);
|
||||
|
||||
// Budget restant pour les charges ECS suivantes (rang supérieur / priorité plus basse).
|
||||
remainingSurplusW = budgetChargeW - la.estimatedPowerW;
|
||||
return la;
|
||||
}
|
||||
|
||||
@ -76,5 +76,20 @@ private:
|
||||
*/
|
||||
LoadAction buildIdleAction(EvCharger *ev) const;
|
||||
|
||||
/*!
|
||||
* \brief Construit un LoadAction "stage" ECS par cascade de surplus (waterfall §6).
|
||||
*
|
||||
* Retient le palier déclaré le plus haut dont la puissance tient dans le budget courant.
|
||||
* Correction B (anti-clignotement) : recrédite d'abord \c lc.telemetry.currentPowerW au
|
||||
* budget (la conso actuelle de l'ECS est déjà soustraite de l'export mesuré), puis
|
||||
* décrémente \c remainingSurplusW de la puissance du palier retenu.
|
||||
*
|
||||
* \param lc Charge ECS (adapter == "relay-stages") du SurplusContext.
|
||||
* \param[in,out] remainingSurplusW Budget de surplus restant (W) ; mis à jour pour la
|
||||
* charge ECS suivante (priorité inférieure / rang supérieur).
|
||||
* \return LoadAction kind=Stage, funding=Surplus, \c reason français non vide.
|
||||
*/
|
||||
LoadAction buildEcsStageAction(const LoadContext &lc, double &remainingSurplusW) const;
|
||||
|
||||
EnergyArbitrator *m_arbitrator;
|
||||
};
|
||||
|
||||
@ -60,8 +60,10 @@ struct LoadNeeds {
|
||||
* L'arbitre lit ce descripteur une fois par cycle pour construire le SurplusContext.
|
||||
* Les valeurs doivent refléter la configuration matérielle réelle (non les setpoints).
|
||||
*
|
||||
* \note \c priority : entier positif, valeur plus haute = traité en premier.
|
||||
* Valeurs suggérées : 200 deadline, 100 normal, 50 confort, 0 stockage différable.
|
||||
* \note \c priority : rang dans la liste ordonnée du client (OPTIMIZER_PROTOCOL §5 +
|
||||
* annexe C). Valeur plus BASSE = servi en premier (rang 1 = premier servi).
|
||||
* Les promotions conditionnelles (deadline, Tempo ROUGE) sont gérées par le scheduler,
|
||||
* pas par un poids numérique.
|
||||
*/
|
||||
struct LoadDescriptor {
|
||||
QString id; //!< ThingId de la charge (string).
|
||||
|
||||
@ -12,7 +12,8 @@
|
||||
/*!
|
||||
* \brief Créneau d'un plan — contient les LoadAction à appliquer pendant [from, to[.
|
||||
*
|
||||
* \invariant Les actions sont ordonnées par priorité décroissante (LoadDescriptor.priority).
|
||||
* \invariant Les actions sont ordonnées par priorité croissante (rang 1 = premier servi,
|
||||
* OPTIMIZER_PROTOCOL §5 + annexe C).
|
||||
* \invariant Un Slot vide (actions vide) est valide — signifie "aucune action ce créneau".
|
||||
*/
|
||||
struct Slot {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user