[3e-3] mapping sémantique SG-Ready + waterfall unifié ECS/SG-Ready
getPlan : cascade unique sur charges non-EV (relay-stages + sg-ready) triées par priorité — budget de surplus partagé. buildSgReadyStateAction : mapping qualitatif (4 forcé hyst. 1,2/1,0 ; 3 reco ≥P3 ; 2 normal mains off ; 1 jamais via surplus), recrédit sur puissance allouée déclarée, clamp lock-aware minState/maxState. AGENTS.md : ROADMAP config priorités utilisateur (acquis tri unifié ; manque 3g VE + couche config JSON-RPC/UI Flutter pour drag-and-drop à chaud). Build 0/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6d7831df9
commit
093fa09b5e
17
AGENTS.md
17
AGENTS.md
@ -370,6 +370,23 @@ supérieures n'affecte pas les couches inférieures. Voir `docs/SAFETY.md` pour
|
||||
(invariants, écrêtage, hypothèses que l'appelant peut faire).
|
||||
Les headers 3a servent de modèle — les convertir au format Doxygen lors du passage 3b.
|
||||
|
||||
## ROADMAP — configuration des priorités par l'utilisateur (post-beta)
|
||||
|
||||
- **ACQUIS (3e)** : le waterfall trie déjà ECS + SG-Ready ensemble par `priority` (rang
|
||||
ASC, 1 = servi en premier), **budget unifié** qui cascade à travers toutes ces charges.
|
||||
- **LIMITE beta** : le VE reste **hors du tri** (décidé par l'amont *avant* le waterfall,
|
||||
décision B) → on ne peut pas classer le VE derrière l'ECS.
|
||||
- **MANQUE pour des priorités réglables par le client** :
|
||||
- (a) **3g** : transplanter le VE dans le waterfall unifié → toutes les charges
|
||||
(VE, ECS, SG-Ready, batterie) classables ensemble.
|
||||
- (b) **Couche « config priorités »** : exposer et persister le `priority` de chaque
|
||||
charge via l'API JSON-RPC, modifiable depuis l'app Flutter (drag-and-drop des
|
||||
priorités de l'UI). C'est le **pont moteur↔UI**, un morceau à part entière
|
||||
(ni 3e ni 3g).
|
||||
- **État actuel** : `priority` est fixé à la création de l'adaptateur (`register*Adapter`),
|
||||
pas d'interface de réglage à chaud.
|
||||
- **Argument démo nymea** : le client réordonne ses charges, le surplus suit.
|
||||
|
||||
## RÉFÉRENCES
|
||||
|
||||
- `docs/OPTIMIZER_PROTOCOL.md` — le contrat. §5 (SurplusContext), §6 (plan/actions),
|
||||
|
||||
@ -96,20 +96,25 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
|
||||
// transitoire sous minOn est géré par le verrou de l'adaptateur, pas par le budget.)
|
||||
double remainingSurplusW = (ctx.meter.exportW - ctx.meter.importW) - 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;
|
||||
// Charges pilotables non-EV (ECS paliers + SG-Ready) triées par priorité ASCENDANTE :
|
||||
// rang 1 = premier servi (OPTIMIZER_PROTOCOL §5 + annexe C — la priorité est un rang).
|
||||
// Le budget de surplus est UNIQUE et cascade à travers TOUTES ces charges par priorité.
|
||||
QList<LoadContext> nonEvLoads;
|
||||
for (const LoadContext &lc : ctx.loads) {
|
||||
if (lc.adapter == QStringLiteral("relay-stages"))
|
||||
ecsLoads.append(lc);
|
||||
if (lc.adapter == QStringLiteral("relay-stages") || lc.adapter == QStringLiteral("sg-ready"))
|
||||
nonEvLoads.append(lc);
|
||||
}
|
||||
std::sort(ecsLoads.begin(), ecsLoads.end(),
|
||||
std::sort(nonEvLoads.begin(), nonEvLoads.end(),
|
||||
[](const LoadContext &a, const LoadContext &b) { return a.priority < b.priority; });
|
||||
|
||||
for (const LoadContext &lc : ecsLoads)
|
||||
slot.actions.append(buildEcsStageAction(lc, remainingSurplusW));
|
||||
for (const LoadContext &lc : nonEvLoads) {
|
||||
if (lc.adapter == QStringLiteral("sg-ready"))
|
||||
slot.actions.append(buildSgReadyStateAction(lc, remainingSurplusW));
|
||||
else
|
||||
slot.actions.append(buildEcsStageAction(lc, remainingSurplusW));
|
||||
}
|
||||
|
||||
// Grid funding ECS : dormant jusqu'à 3f (waterfall réseau) — non implémenté ici.
|
||||
// Grid funding (ECS/PAC) : dormant jusqu'à 3f (waterfall réseau) — non implémenté ici.
|
||||
|
||||
Plan plan;
|
||||
plan.planId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
@ -217,3 +222,67 @@ LoadAction RuleBasedScheduler::buildEcsStageAction(const LoadContext &lc,
|
||||
remainingSurplusW = budgetChargeW - la.estimatedPowerW;
|
||||
return la;
|
||||
}
|
||||
|
||||
LoadAction RuleBasedScheduler::buildSgReadyStateAction(const LoadContext &lc,
|
||||
double &remainingSurplusW) const
|
||||
{
|
||||
const int currentState = lc.telemetry.state;
|
||||
const double p3 = lc.declared.estimatedPowerW.value(3, 0.0);
|
||||
const double p4 = lc.declared.estimatedPowerW.value(4, 0.0);
|
||||
|
||||
// Recrédit (correction B) : la puissance ALLOUÉE de l'état courant (déclaré, 0 pour 1/2)
|
||||
// revient au budget — comme l'ECS. Base : "quel surplus si la PAC n'était pas pilotée ?"
|
||||
const double allocatedNowW = lc.declared.estimatedPowerW.value(currentState, 0.0);
|
||||
const double budgetW = remainingSurplusW + allocatedNowW;
|
||||
|
||||
// Mapping SÉMANTIQUE (pas "le plus haut qui rentre") avec hystérésis d'état anti-oscillation :
|
||||
// monter en 4 si budget ≥ P4×1,2 ; rester en 4 tant que budget ≥ P4×1,0 (zone morte) ;
|
||||
// sinon recommandation (≥ P3) ; sinon normal (état 2, mains off).
|
||||
// État 1 (effacement) jamais déclenché par le surplus seul — déféré (signal tarif/réseau).
|
||||
constexpr double kForceMargin = 1.2; // hystérésis d'entrée en état 4
|
||||
int targetState;
|
||||
if (p4 > 0.0 && budgetW >= p4 * kForceMargin)
|
||||
targetState = 4;
|
||||
else if (currentState == 4 && p4 > 0.0 && budgetW >= p4) // zone morte : reste forcé
|
||||
targetState = 4;
|
||||
else if (p3 > 0.0 && budgetW >= p3)
|
||||
targetState = 3;
|
||||
else
|
||||
targetState = 2;
|
||||
|
||||
// Clamp lock-aware (fenêtre minStateHold, calculée au MÊME ctx.timestamp).
|
||||
const int loW = lc.telemetry.minState > 0 ? lc.telemetry.minState : 2;
|
||||
const int hiW = lc.telemetry.maxState > 0 ? lc.telemetry.maxState : 2;
|
||||
int bestState = qBound(qMin(loW, hiW), targetState, qMax(loW, hiW));
|
||||
// Sécurité : ne jamais commander un état non déclaré (snap au plus haut déclaré ≤ cible).
|
||||
if (!lc.declared.states.contains(bestState)) {
|
||||
int snapped = lc.declared.states.isEmpty() ? 2 : lc.declared.states.first();
|
||||
for (int s : lc.declared.states)
|
||||
if (s <= bestState && s > snapped) snapped = s;
|
||||
bestState = snapped;
|
||||
}
|
||||
|
||||
LoadAction la;
|
||||
la.loadId = lc.id;
|
||||
la.kind = LoadAction::State;
|
||||
la.funding = LoadAction::Surplus;
|
||||
la.state = bestState;
|
||||
la.estimatedPowerW = lc.declared.estimatedPowerW.value(bestState, 0.0);
|
||||
|
||||
if (bestState != targetState)
|
||||
la.reason = QStringLiteral("Verrou minStateHold — %1 maintenue état %2 (court-cycling PAC)")
|
||||
.arg(lc.label).arg(bestState);
|
||||
else if (bestState == 4)
|
||||
la.reason = QStringLiteral("Surplus abondant %1 W — %2 forcée (état 4, ~%3 W)")
|
||||
.arg(qRound(budgetW)).arg(lc.label).arg(qRound(p4));
|
||||
else if (bestState == 3)
|
||||
la.reason = QStringLiteral("Surplus PV %1 W — %2 recommandée (état 3, ~%3 W)")
|
||||
.arg(qRound(budgetW)).arg(lc.label).arg(qRound(p3));
|
||||
else
|
||||
la.reason = QStringLiteral("Surplus insuffisant (%1 W) — %2 en normal (état 2, non pilotée)")
|
||||
.arg(qRound(budgetW)).arg(lc.label);
|
||||
|
||||
// Budget restant : on ne soustrait que la puissance ALLOUÉE (états 1/2 = 0).
|
||||
remainingSurplusW = budgetW - la.estimatedPowerW;
|
||||
return la;
|
||||
}
|
||||
|
||||
@ -91,5 +91,20 @@ private:
|
||||
*/
|
||||
LoadAction buildEcsStageAction(const LoadContext &lc, double &remainingSurplusW) const;
|
||||
|
||||
/*!
|
||||
* \brief Construit un LoadAction "state" SG-Ready (PAC) par mapping SÉMANTIQUE du surplus.
|
||||
*
|
||||
* 4 états normés (qualitatifs, pas des paliers) : surplus abondant stable → 4 (forcé,
|
||||
* hystérésis P4×1,2 entrée / P4×1,0 sortie) ; surplus durable → 3 (recommandation, ≥P3) ;
|
||||
* sinon → 2 (normal, mains off). L'état 1 (effacement) n'est PAS déclenché par le surplus
|
||||
* seul (déféré : signal tarif/réseau). Recrédit (correction B) sur la puissance allouée
|
||||
* (déclaré, 0 pour 1/2). Clamp lock-aware via \c minState/maxState (court-cycling PAC).
|
||||
*
|
||||
* \param lc Charge SG-Ready (adapter == "sg-ready") du SurplusContext.
|
||||
* \param[in,out] remainingSurplusW Budget de surplus restant (W), mis à jour pour la suite.
|
||||
* \return LoadAction kind=State, funding=Surplus, \c reason français non vide.
|
||||
*/
|
||||
LoadAction buildSgReadyStateAction(const LoadContext &lc, double &remainingSurplusW) const;
|
||||
|
||||
EnergyArbitrator *m_arbitrator;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user