From f134048cfb3433dfb18950d1efbd181caea01559 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Wed, 26 Aug 2026 11:02:50 +0200 Subject: [PATCH] =?UTF-8?q?fix(motifs):=20BELOW=5FMIN=5FPOWER=20l=C3=A0=20?= =?UTF-8?q?o=C3=B9=20le=20m=C3=A9canisme=20conna=C3=AEt=20la=20cause=20pr?= =?UTF-8?q?=C3=A9cise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H --- INTERFACE.md | 10 +++++-- debian-qt5/changelog | 29 ++++++++++++++++++ .../etm/scheduler/rulebasedscheduler.cpp | 30 +++++++++++++++++-- energyplugin/etm/types/decisionreason.cpp | 18 +++++++++-- energyplugin/etm/types/decisionreason.h | 21 +++++++++---- 5 files changed, 94 insertions(+), 14 deletions(-) diff --git a/INTERFACE.md b/INTERFACE.md index c7bd817..be4d206 100644 --- a/INTERFACE.md +++ b/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) | diff --git a/debian-qt5/changelog b/debian-qt5/changelog index 1ab463c..940ad5b 100644 --- a/debian-qt5/changelog +++ b/debian-qt5/changelog @@ -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 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 diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.cpp b/energyplugin/etm/scheduler/rulebasedscheduler.cpp index fb7413c..45e8cf4 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.cpp +++ b/energyplugin/etm/scheduler/rulebasedscheduler.cpp @@ -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). diff --git a/energyplugin/etm/types/decisionreason.cpp b/energyplugin/etm/types/decisionreason.cpp index 00915b2..3a400cd 100644 --- a/energyplugin/etm/types/decisionreason.cpp +++ b/energyplugin/etm/types/decisionreason.cpp @@ -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 ------------------------------------------ diff --git a/energyplugin/etm/types/decisionreason.h b/energyplugin/etm/types/decisionreason.h index 72a5f89..9fa023c 100644 --- a/energyplugin/etm/types/decisionreason.h +++ b/energyplugin/etm/types/decisionreason.h @@ -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";