fix(motifs): BELOW_MIN_POWER là où le mécanisme connaît la cause précise
Audit demandé par Patrick : un motif qui décrit la cause générique là où le mécanisme connaît la cause précise. Deux branches en portaient le défaut, et une troisième le porte ailleurs. Branche à paliers : un budget de 300 W devant un premier palier de 500 W publiait SURPLUS_INSUFFICIENT. Ce n'est pas un manque de surplus, c'est une granularité. Branche sg-ready : le code publié était SG_NORMAL — correct sur le fond, donc l'app n'a jamais été trompée. C'est le RENDU FRANÇAIS qui disait « Surplus insuffisant (933 W) » devant un état 3 à 1 500 W, et c'est l'installateur qui lit journalctl pendant une mise en service qui l'était. Le paramètre state nomme désormais l'état hors d'atteinte : dire « sous le plancher » sans dire lequel remplacerait une phrase vague par une autre. La distinction conservée est celle que Patrick a posée : budget ≤ 0 laisse SURPLUS_INSUFFICIENT / SG_NORMAL — il n'y a rien. Budget positif sous le premier palier donne BELOW_MIN_POWER — il y en a, mais pas au bon format. Troisième cas, non corrigé ici : EV_IDLE dit « pas de surplus » là où la borne a un plancher de 6 A. Son site est le proxy amont, que 3g remplace ; l'y corriger maintenant reviendrait à écrire deux fois la même règle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
This commit is contained in:
parent
c94e78a194
commit
f134048cfb
10
INTERFACE.md
10
INTERFACE.md
@ -352,6 +352,12 @@ un ensemble **discontinu**. C'est ce que fait le matériel réel — une borne n
|
||||
règle, une charge de rang 1 au plancher élevé retiendrait en otage un budget qu'elle ne peut
|
||||
pas utiliser, et tout le waterfall en aval resterait à sec pendant qu'elle-même ne démarre pas.
|
||||
|
||||
> **« Il n'y a pas assez » contre « il y en a, mais pas au bon format ».** C'est toute la
|
||||
> distinction que le motif `BELOW_MIN_POWER` porte, et elle décide du geste :
|
||||
> `SURPLUS_INSUFFICIENT` et `SG_NORMAL` disent qu'il n'y a **rien** (budget ≤ 0) — on attend le
|
||||
> soleil. `BELOW_MIN_POWER` dit qu'il y a du budget mais qu'**aucun palier ne sait le prendre**
|
||||
> — on relit une fiche technique.
|
||||
|
||||
> **Le cas qui rend la règle nécessaire** : 2 kW de surplus, deux bornes à 1,38 kW de plancher
|
||||
> (6 A monophasé). Réparties également, 1 kW chacune, **aucune ne démarre et les 2 kW partent au
|
||||
> réseau**. En cascade, la première prend 1,38 kW et charge ; la seconde reçoit le reliquat,
|
||||
@ -513,11 +519,11 @@ le **libellé de la charge n'y figure jamais** (le client le résout via `GetLoa
|
||||
| `LOCK_MIN_OFF_CAPPED` | `appliedW`, `budgetW`, `requestedW` | Le verrou `minOff` plafonne sans interdire |
|
||||
| `SURPLUS_SETPOINT` | `budgetW`, `setpointW`, `stepped` | Consigne servie par le surplus ; `stepped` = arrondie sur un palier déclaré |
|
||||
| `SURPLUS_INSUFFICIENT` | `budgetW` | Le budget ne payait aucun palier — **aucun verrou n'a mordu** |
|
||||
| `BELOW_MIN_POWER` | `budgetW`, `minPowerW` | Budget **sous le plancher** de cette charge : 0 W, et le budget passe à la suivante |
|
||||
| `BELOW_MIN_POWER` | `budgetW`, `minPowerW`, `o:state` | Budget **sous le premier palier atteignable** : 0 W, et le budget passe à la suivante. `state` nommé pour les mécanismes à états |
|
||||
| `LOCK_MIN_STATE_HOLD` | `state` | Verrou `minStateHold` : état PAC maintenu (protection court-cycling) |
|
||||
| `SG_FORCED` | `budgetW`, `estimatedW` | PAC en état 4 (forcé) |
|
||||
| `SG_RECOMMENDED` | `budgetW`, `estimatedW` | PAC en état 3 (recommandation) |
|
||||
| `SG_NORMAL` | `budgetW` | PAC en état 2 (normal, mains off) |
|
||||
| `SG_NORMAL` | `budgetW` | PAC en état 2 : **aucun surplus à allouer** (budget ≤ 0). Un budget positif mais sous l'état 3 donne `BELOW_MIN_POWER`, pas celui-ci |
|
||||
| `DEGRADED_L2` | — | Consigne de repli du watchdog L2 (compteur muet) |
|
||||
| `SAFE_STATE_RELAY` | — | État sûr avant retrait : tous relais ouverts (ECS-413) |
|
||||
| `SAFE_STATE_SETPOINT` | — | État sûr avant retrait : consigne 0 W (ECS-413) |
|
||||
|
||||
@ -1,3 +1,32 @@
|
||||
powersync-energy-plugin-nymea (1.15.2+etm19) trixie; urgency=medium
|
||||
|
||||
* §12 suite — BELOW_MIN_POWER étendu aux deux branches qui connaissaient la cause précise
|
||||
et publiaient la générique. La distinction est « il n'y a pas assez » contre « il y en a,
|
||||
mais pas au bon format », et elle décide du geste : attendre le soleil, ou relire une
|
||||
fiche technique.
|
||||
* Branche à paliers (relay-router, etmvariableload fixed) : un budget de 300 W devant un
|
||||
premier palier de 500 W n'est pas un manque de surplus, c'est une granularité. Elle
|
||||
publiait SURPLUS_INSUFFICIENT ; elle publie désormais BELOW_MIN_POWER avec le palier
|
||||
hors d'atteinte.
|
||||
* Branche sg-ready : elle publiait SG_NORMAL — correct sur le fond, mais son rendu français
|
||||
disait « Surplus insuffisant (933 W) » devant un état 3 à 1 500 W. Le code publié n'a
|
||||
jamais trompé l'app ; c'est l'installateur qui lit journalctl pendant une mise en service
|
||||
qui l'était. Un budget positif sous l'état 3 donne maintenant BELOW_MIN_POWER, avec le
|
||||
paramètre state qui NOMME l'état hors d'atteinte : dire « sous le plancher » sans dire
|
||||
lequel remplacerait une phrase vague par une autre.
|
||||
* SG_NORMAL ne sort donc plus que pour un budget nul ou négatif, et sa phrase devient
|
||||
« Aucun surplus à allouer » — vraie dans ce seul cas, là où « surplus insuffisant »
|
||||
couvrait aussi, et à tort, un budget de 933 W.
|
||||
* SURPLUS_INSUFFICIENT est conservé tel quel pour budget ≤ 0 : il reste juste quand il n'y a
|
||||
réellement rien.
|
||||
* ⚠️ CHANGEMENT DE CONTRAT — BELOW_MIN_POWER est un code nouveau, et son paramètre state est
|
||||
optionnel. Un client sans clé de rendu affichera son repli.
|
||||
* Le cas EV porte le même défaut (EV_IDLE dit « pas de surplus » là où la borne a un plancher
|
||||
de 6 A), mais son site est le proxy amont, que la transplantation 3g remplace. Corrigé là
|
||||
plutôt qu'ici, sans quoi on écrirait deux fois la même règle.
|
||||
|
||||
-- Patrick Schurig <etm.schurig@gmail.com> Wed, 26 Aug 2026 14:30:00 +0200
|
||||
|
||||
powersync-energy-plugin-nymea (1.15.2+etm18) trixie; urgency=medium
|
||||
|
||||
* Lot E §12 — le plancher de modulation. Le domaine d'une consigne cesse d'être
|
||||
|
||||
@ -202,7 +202,8 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
const bool dynamic = levels.isEmpty();
|
||||
|
||||
double setpointW;
|
||||
bool sousPlancher = false;
|
||||
bool sousPlancher = false; //!< Budget > 0 mais sous le premier palier atteignable.
|
||||
double plancherW = 0; //!< Ce palier, précisément — le motif doit le NOMMER.
|
||||
if (dynamic) {
|
||||
// Dynamic (routeur PV / triac) : modulation continue bornée au plafond déclaré.
|
||||
const double maxW = lc.declared.maxPowerW > 0 ? lc.declared.maxPowerW : budgetW;
|
||||
@ -218,6 +219,7 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
// servir la première à fond en fait charger une.
|
||||
if (lc.declared.minPowerW > 0 && setpointW > 0 && setpointW < lc.declared.minPowerW) {
|
||||
setpointW = 0;
|
||||
plancherW = lc.declared.minPowerW;
|
||||
sousPlancher = true;
|
||||
}
|
||||
} else {
|
||||
@ -229,6 +231,19 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
else break;
|
||||
}
|
||||
setpointW = chosen;
|
||||
|
||||
// §12 — même distinction qu'en dynamic, et le plancher y est DÉRIVÉ : levels[0] vaut
|
||||
// toujours 0, donc le premier palier atteignable est levels[1].
|
||||
//
|
||||
// Ce qui se joue ici est la différence entre « il n'y a pas assez » et « il y en a,
|
||||
// mais pas au bon format ». Un budget de 300 W devant un premier palier de 500 W
|
||||
// n'est pas un manque de surplus : c'est une granularité. Les deux phrases envoient
|
||||
// l'installateur vers des gestes opposés — attendre le soleil, ou relire son câblage.
|
||||
const int premier = levels.size() > 1 ? levels.at(1) : 0;
|
||||
if (premier > 0 && budgetW > 0 && chosen == 0) {
|
||||
plancherW = premier;
|
||||
sousPlancher = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ECS-410 — charge INDISPONIBLE : elle sort de l'ARBITRAGE, pas de la COMPTABILITÉ.
|
||||
@ -312,8 +327,7 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
// §12 — dire QUE le budget est sous le plancher, pas qu'il est insuffisant : la
|
||||
// seconde phrase enverrait chercher du soleil là où il faut lire une fiche technique.
|
||||
la.reason = { DecisionCode::BelowMinPower,
|
||||
{{"budgetW", qRound(budgetW)},
|
||||
{"minPowerW", qRound(lc.declared.minPowerW)}} };
|
||||
{{"budgetW", qRound(budgetW)}, {"minPowerW", qRound(plancherW)}} };
|
||||
else
|
||||
la.reason = { DecisionCode::SurplusInsufficient, {{"budgetW", qRound(budgetW)}} };
|
||||
|
||||
@ -379,7 +393,17 @@ LoadAction RuleBasedScheduler::buildSgReadyStateAction(const LoadContext &lc,
|
||||
else if (bestState == 3)
|
||||
la.reason = { DecisionCode::SgRecommended,
|
||||
{{"budgetW", qRound(budgetW)}, {"estimatedW", qRound(p3)}} };
|
||||
else if (budgetW > 0 && p3 > 0)
|
||||
// §12 — il y a du budget, mais aucun état ne sait le prendre : une PAC SG-Ready n'a
|
||||
// pas d'« état 3 à moitié ». Le motif NOMME l'état hors d'atteinte et sa puissance
|
||||
// estimée — dire « minPowerW » sans dire lequel remplacerait une phrase vague par
|
||||
// une autre.
|
||||
la.reason = { DecisionCode::BelowMinPower,
|
||||
{{"budgetW", qRound(budgetW)}, {"minPowerW", qRound(p3)},
|
||||
{"state", 3}} };
|
||||
else
|
||||
// Budget nul ou négatif : il n'y a réellement RIEN à allouer. « Surplus insuffisant »
|
||||
// est alors la vérité, et c'est le seul cas où elle l'est.
|
||||
la.reason = { DecisionCode::SgNormal, {{"budgetW", qRound(budgetW)}} };
|
||||
|
||||
// Budget restant : on ne soustrait que la puissance ALLOUÉE (états 1/2 = 0).
|
||||
|
||||
@ -66,10 +66,18 @@ QString renderFr(const DecisionReason &reason, const QString &label)
|
||||
.arg(w("budgetW")).arg(label);
|
||||
// §12 — la phrase nomme le PLANCHER, pas le manque de surplus : le budget peut être
|
||||
// confortable et rester sous le minimum que ce matériel accepte.
|
||||
if (c == QLatin1String(DecisionCode::BelowMinPower))
|
||||
return QStringLiteral("Budget %1 W sous le plancher de %2 (%3 W) — 0 W, "
|
||||
if (c == QLatin1String(DecisionCode::BelowMinPower)) {
|
||||
// Quand le mécanisme a des ÉTATS, dire lequel était hors d'atteinte : « sous le
|
||||
// plancher » sans nommer le palier remplacerait une phrase vague par une autre.
|
||||
if (p.contains(QStringLiteral("state")))
|
||||
return QStringLiteral("Budget %1 W sous l'état %2 de %3 (%4 W estimés) — état 2, "
|
||||
"le budget passe à la charge suivante")
|
||||
.arg(w("budgetW")).arg(p.value(QStringLiteral("state")).toInt())
|
||||
.arg(label).arg(w("minPowerW"));
|
||||
return QStringLiteral("Budget %1 W sous le premier palier de %2 (%3 W) — 0 W, "
|
||||
"le budget passe à la charge suivante")
|
||||
.arg(w("budgetW")).arg(label).arg(w("minPowerW"));
|
||||
}
|
||||
|
||||
// --- SG-Ready (PAC) -----------------------------------------------------
|
||||
if (c == QLatin1String(DecisionCode::LockMinStateHold))
|
||||
@ -82,7 +90,11 @@ QString renderFr(const DecisionReason &reason, const QString &label)
|
||||
return QStringLiteral("Surplus PV %1 W — %2 recommandée (état 3, ~%3 W)")
|
||||
.arg(w("budgetW")).arg(label).arg(w("estimatedW"));
|
||||
if (c == QLatin1String(DecisionCode::SgNormal))
|
||||
return QStringLiteral("Surplus insuffisant (%1 W) — %2 en normal (état 2, non pilotée)")
|
||||
// §12 — depuis que BELOW_MIN_POWER porte le cas « budget présent mais sous l'état 3 »,
|
||||
// celui-ci ne sort plus QUE pour un budget nul ou négatif. La phrase peut donc dire
|
||||
// « aucun surplus » sans mentir, là où « surplus insuffisant » couvrait aussi, et à
|
||||
// tort, un budget de 933 W devant un état à 1 500 W.
|
||||
return QStringLiteral("Aucun surplus à allouer (%1 W) — %2 en normal (état 2, non pilotée)")
|
||||
.arg(w("budgetW")).arg(label);
|
||||
|
||||
// --- Sécurité et cycle de vie ------------------------------------------
|
||||
|
||||
@ -62,13 +62,22 @@ constexpr const char *SurplusSetpoint = "SURPLUS_SETPOINT";
|
||||
//! params: \c budgetW — le budget ne payait aucun palier ; aucun verrou n'a mordu.
|
||||
constexpr const char *SurplusInsufficient = "SURPLUS_INSUFFICIENT";
|
||||
/*!
|
||||
* \brief params: \c budgetW, \c minPowerW — le budget est sous le PLANCHER de la charge.
|
||||
* \brief params: \c budgetW, \c minPowerW, \c o:state — le budget est sous le PLANCHER.
|
||||
*
|
||||
* §12 / LM-1203. Distinct de \c SURPLUS_INSUFFICIENT, qui dit « le budget ne payait aucun
|
||||
* palier ». Ici le budget est peut-être confortable dans l'absolu — il est simplement sous le
|
||||
* minimum que CE matériel accepte, et la charge a donc reçu 0 pendant que le budget partait à
|
||||
* la suivante. Les confondre enverrait diagnostiquer un manque de soleil là où il faut lire
|
||||
* une fiche technique (ECS-309 : le motif nomme le mécanisme qui a produit l'issue).
|
||||
* §12 / LM-1203. La distinction qu'il porte est **« il n'y a pas assez » contre « il y en a,
|
||||
* mais pas au bon format »** :
|
||||
*
|
||||
* | situation | motif |
|
||||
* |---|---|
|
||||
* | budget ≤ 0 | \c SURPLUS_INSUFFICIENT / \c SG_NORMAL — il n'y a rien |
|
||||
* | 0 < budget < premier palier atteignable | **\c BELOW_MIN_POWER** — il y a, mais pas au bon format |
|
||||
*
|
||||
* Les confondre envoie diagnostiquer un manque de soleil là où il faut lire une fiche
|
||||
* technique (ECS-309 : le motif nomme le mécanisme qui a produit l'issue).
|
||||
*
|
||||
* \note \c state n'est présent que pour les mécanismes à ÉTATS (sg-ready), où il nomme
|
||||
* l'état hors d'atteinte. Dire « sous le plancher » sans dire lequel remplacerait une
|
||||
* phrase vague par une autre.
|
||||
*/
|
||||
constexpr const char *BelowMinPower = "BELOW_MIN_POWER";
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user