diff --git a/debian-qt5/changelog b/debian-qt5/changelog index 940ad5b..6c49827 100644 --- a/debian-qt5/changelog +++ b/debian-qt5/changelog @@ -1,3 +1,33 @@ +powersync-energy-plugin-nymea (1.15.2+etm20) trixie; urgency=medium + + * 3g-1, première moitié — la prise en compte du stockage monte dans le budget. Elle vivait + dans planSurplusCharging(), donc dans le chemin VE, par accident d'histoire : ce n'a jamais + été une règle « véhicule » mais une règle de BUDGET. Tant qu'elle y restait, une batterie + sous sa réserve empêchait de charger la voiture et laissait le chauffe-eau se servir. + * ⚠️ CHANGEMENT DE COMPORTEMENT pour l'ECS et la PAC, qui n'ont jamais connu cette règle. + Une installation qui servait ses charges sous le seuil de réserve ne le fera plus. + * ⚠️ DÉFAUT D'USINE ABAISSÉ : batteryLevelConsideration passe de 0,9 à 0,2. Le réglage + confondait une CIBLE et une RÉSERVE — une cible est ce qu'on aimerait atteindre, une + réserve ce sous quoi on ne descend pas, et la règle qu'il pilote a une sémantique de + réserve. Tant qu'elle ne touchait que le VE, 0,9 se défendait ; depuis qu'elle gouverne + toute l'installation, 0,9 gèle tout sous 90 % de SOC : ni ECS, ni PAC, ni borne. Chez un + client cela se manifeste comme un HEMS qui ne pilote plus rien, sans erreur et sans panne — + le pire mode de défaillance, parce qu'il ne se signale pas. Les installations existantes + conservent leur valeur enregistrée ; seul le défaut change. + * Motif BATTERY_RESERVE (socPercent, reservePercent, withheldW) — troisième terme de la + distinction déjà portée par BELOW_MIN_POWER : il n'y a rien / il y en a mais pas au bon + format / il y en a et une RÈGLE l'interdit. Sans lui, une installation qui exporte en plein + soleil n'enclenche rien et publie « surplus insuffisant » : l'installateur cherche une panne + qui n'existe pas. withheldW dit combien de surplus a été retenu — c'est ce chiffre, et non + le SOC, qui rend la cause lisible. + * Sous la réserve, le budget est ANNULÉ et non réduit (LM-1203-b). Servir « un peu » + retarderait la recharge du stockage sans démarrer personne, puisque les planchers du §12 + mordent d'abord. + * ⚠️ CHANGEMENT DE CONTRAT — BATTERY_RESERVE est un code nouveau : un client sans clé de + rendu affichera son repli. + + -- Patrick Schurig Wed, 26 Aug 2026 16:00:00 +0200 + 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 diff --git a/docs/DESIGN_3g.md b/docs/DESIGN_3g.md index 8959969..9359929 100644 --- a/docs/DESIGN_3g.md +++ b/docs/DESIGN_3g.md @@ -149,6 +149,26 @@ qui bascule en triphasé change son plancher effectif (6 A × 3 = 4,1 kW au lieu --- +## 4-bis. Vérifié : `evReservedW` après la transplantation + +`evReservedW` (correction A) existe parce que l'EV est décidé **avant** le waterfall : sa +consigne du cycle n'est pas encore visible au compteur, donc on la réserve. Une fois le VE dans +la cascade, sa réservation **devient** son allocation — la compter en plus serait un double +comptage. + +**Audit du financement de chaque action EV** (`rulebasedscheduler.cpp`) : + +| action | financement | après 3g-1 | +|---|---|---| +| spot market (l. 61) | `Grid` | reste au proxy → **doit rester dans `evReservedW`** | +| `buildTimeRequirementAction` (l. 202) | `Grid` | reste au proxy → **doit rester** | +| `buildMinCurrentAction` (l. 219) | `Surplus` | **disparaît** — remplacé par la cascade | +| `buildIdleAction` (l. 233) | `Surplus` | **disparaît** — remplacé par les motifs du §12 | + +**Conclusion** : le filtre de `evReservedW` doit devenir `funding == LoadAction::Grid`. Il ne +s'agit pas de le supprimer mais de le **restreindre** à ce qui reste hors cascade. Tant que la +seconde moitié de 3g-1 n'est pas faite, il reste correct tel quel. + ## 5. Découpage proposé | Étape | Contenu | Éprouvable | diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.cpp b/energyplugin/etm/scheduler/rulebasedscheduler.cpp index 5b664df..310eae8 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.cpp +++ b/energyplugin/etm/scheduler/rulebasedscheduler.cpp @@ -104,10 +104,22 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) // // ⚠️ CHANGEMENT DE COMPORTEMENT pour l'ECS et la PAC, qui n'ont jamais connu cette règle. // Une installation qui servait ses charges sous le seuil de réserve ne le fera plus. + // Retenu pour le MOTIF : sans lui, une installation qui exporte en plein soleil + // n'enclencherait rien en publiant « surplus insuffisant », et l'installateur chercherait + // une panne qui n'existe pas. + m_reserveActive = false; + m_reserveSocPct = 0; + m_reserveSeuilPct = 0; + m_reserveWithheldW = 0; + if (ctx.battery.present) { const double seuil = m_arbitrator->batteryLevelConsideration() * 100.0; if (ctx.battery.socPercent < seuil) { + m_reserveActive = true; + m_reserveSocPct = ctx.battery.socPercent; + m_reserveSeuilPct = seuil; + m_reserveWithheldW = qMax(0.0, remainingSurplusW); // Sous la réserve : le stockage passe AVANT toute charge pilotée. On ne réduit pas // le budget, on l'annule — servir « un peu » reviendrait à retarder la recharge de // la batterie tout en ne démarrant probablement personne (planchers du §12). @@ -362,6 +374,14 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc, la.reason = { DecisionCode::SurplusSetpoint, {{"budgetW", qRound(budgetW)}, {"setpointW", qRound(setpointW)}, {"stepped", !dynamic}} }; + else if (m_reserveActive) + // 3g-1 — il Y A du surplus, et une règle l'interdit. À ne pas confondre avec + // « il n'y en a pas » : le geste attendu n'est pas le même, et le second envoie + // chercher une panne inexistante. + la.reason = { DecisionCode::BatteryReserve, + {{"socPercent", qRound(m_reserveSocPct)}, + {"reservePercent", qRound(m_reserveSeuilPct)}, + {"withheldW", qRound(m_reserveWithheldW)}} }; else if (sousPlancher) // §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. @@ -432,6 +452,11 @@ LoadAction RuleBasedScheduler::buildSgReadyStateAction(const LoadContext &lc, else if (bestState == 3) la.reason = { DecisionCode::SgRecommended, {{"budgetW", qRound(budgetW)}, {"estimatedW", qRound(p3)}} }; + else if (m_reserveActive) + la.reason = { DecisionCode::BatteryReserve, + {{"socPercent", qRound(m_reserveSocPct)}, + {"reservePercent", qRound(m_reserveSeuilPct)}, + {"withheldW", qRound(m_reserveWithheldW)}} }; 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 diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.h b/energyplugin/etm/scheduler/rulebasedscheduler.h index 8ebb4a6..cf860b7 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.h +++ b/energyplugin/etm/scheduler/rulebasedscheduler.h @@ -125,4 +125,13 @@ private: PlanBudget &budget) const; EnergyArbitrator *m_arbitrator; + + // --- Réserve batterie : retenu d'un cycle pour le MOTIF (3g-1) ----------------------- + //! Vrai quand le budget de surplus a été confisqué par la réserve de stockage. + bool m_reserveActive = false; + double m_reserveSocPct = 0; //!< SOC constaté au moment de la confiscation. + double m_reserveSeuilPct = 0; //!< Seuil configuré, en pourcentage. + //! Surplus qui EXISTAIT et n'a pas été distribué. C'est ce chiffre, pas le SOC, qui rend + //! la cause lisible : « 3 200 W retenus » se comprend là où « batterie à 47 % » se subit. + double m_reserveWithheldW = 0; }; diff --git a/energyplugin/etm/types/decisionreason.cpp b/energyplugin/etm/types/decisionreason.cpp index 3a400cd..931cf16 100644 --- a/energyplugin/etm/types/decisionreason.cpp +++ b/energyplugin/etm/types/decisionreason.cpp @@ -66,6 +66,14 @@ 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::BatteryReserve)) + // La phrase nomme d'abord ce qui a été RETENU : c'est le chiffre qui rend la cause + // lisible, là où un pourcentage de SOC se subit sans se comprendre. + return QStringLiteral("%1 W de surplus retenus — batterie à %2 %%, sous la réserve de " + "%3 %% ; aucune charge n'est servie") + .arg(w("withheldW")) + .arg(p.value(QStringLiteral("socPercent")).toInt()) + .arg(p.value(QStringLiteral("reservePercent")).toInt()); 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. diff --git a/energyplugin/etm/types/decisionreason.h b/energyplugin/etm/types/decisionreason.h index 9fa023c..103c510 100644 --- a/energyplugin/etm/types/decisionreason.h +++ b/energyplugin/etm/types/decisionreason.h @@ -61,6 +61,24 @@ constexpr const char *LockMinOffCapped = "LOCK_MIN_OFF_CAPPED"; 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 socPercent, \c reservePercent, \c withheldW — le budget est CONFISQUÉ. + * + * 3g-1. Troisième terme de la distinction que porte déjà \c BELOW_MIN_POWER, et le plus + * traître des trois parce qu'il ne se déduit d'aucune mesure : + * + * | situation | motif | + * |---|---| + * | il n'y a rien | \c SURPLUS_INSUFFICIENT | + * | il y en a, pas au bon format | \c BELOW_MIN_POWER | + * | il y en a, et une RÈGLE l'interdit | **\c BATTERY_RESERVE** | + * + * Sans ce motif, une installation par grand soleil n'enclenche rien et publie « surplus + * insuffisant » alors que le compteur exporte : l'installateur cherche une panne qui n'existe + * pas. \c withheldW dit combien de surplus a été retenu — c'est ce chiffre, et non le SOC, + * qui rend la cause lisible en une ligne. + */ +constexpr const char *BatteryReserve = "BATTERY_RESERVE"; /*! * \brief params: \c budgetW, \c minPowerW, \c o:state — le budget est sous le PLANCHER. * diff --git a/energyplugin/smartchargingmanager.cpp b/energyplugin/smartchargingmanager.cpp index c3d2665..22a9e48 100644 --- a/energyplugin/smartchargingmanager.cpp +++ b/energyplugin/smartchargingmanager.cpp @@ -44,7 +44,23 @@ SmartChargingManager::SmartChargingManager(EnergyManager *energyManager, ThingMa EnergySettings settings; m_phasePowerConsumptionLimit = settings.value("phasePowerConsumptionLimit", 0).toInt(); m_acquisitionTolerance = settings.value("acquisitionTolerance", 0.5).toDouble(); - m_batteryLevelConsideration = settings.value("batteryLevelConsideration", 0.9).toDouble(); + // 3g-1 — DÉFAUT ABAISSÉ de 0,9 à 0,2, et la raison n'est pas cosmétique. + // + // Ce réglage confondait une CIBLE et une RÉSERVE. Une cible, c'est le niveau qu'on aimerait + // atteindre ; une réserve, c'est le niveau sous lequel on ne descend pas. La règle qu'il + // pilote — « sous ce niveau, rien d'autre n'est servi » — a une sémantique de réserve, et + // elle était défaultée comme une cible. + // + // Tant que la règle ne touchait que le VE, 0,9 se défendait : « ne pas vider la batterie + // dans une voiture ». Depuis 3g-1 elle gouverne TOUTE l'installation, et 0,9 y devient un + // gel : sous 90 % de SOC, ni ECS, ni PAC, ni borne. Chez un client cela se manifeste comme + // un HEMS qui ne pilote plus rien, sans erreur et sans panne — le pire mode de défaillance + // qui soit, parce qu'il ne se signale pas. + // + // 0,2 protège ce qu'une réserve doit protéger — l'autonomie du soir — sans confisquer la + // journée. Un installateur qui veut « la batterie d'abord » monte la valeur délibérément, + // maintenant que l'écran l'expose. + m_batteryLevelConsideration = settings.value("batteryLevelConsideration", 0.2).toDouble(); settings.beginGroup("ChargingInfos"); foreach (const QString &evChargerIdString, settings.childGroups()) { diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index a0548cc..b7b6541 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -1,12 +1,16 @@ # SPEC — Modèle de charges (domaine × mécanisme) -Version : 0.2.2 +Version : 0.2.3 Dépôt : `etm-powersync-energy-plugin-etm` Statut : **intention de conception figée.** §3 (schéma persisté) est **en cours d'implémentation** depuis le 2026-08-09 : `relay-router`, `etmvariableload` et `sg-ready` sont trois charges utiles discriminées par `adapter`. Le reste du document reste une intention. +> **0.2.3** — LM-1203-b : sous la réserve de stockage, le budget est annulé et non réduit ; +> avec son corollaire — une réserve réglée haut gèle toute l'installation sans rien signaler — +> et les deux exigences qui l'accompagnent (motif dédié, défaut d'usine ramené de 0,9 à 0,2). + > **0.2.2** — LM-1204-b : le scénario d'acceptation de 3g, avec son piège de simulateur et le > motif qui le déjoue. Ajouté quand le banc a reçu une seconde borne — et **il reste > inexerçable tant que 3g n'est pas fait**, ce qui est précisément ce qu'il mesure. @@ -750,6 +754,25 @@ Le proxy actuel, qui décide du VE **en amont** du waterfall, ne peut pas produi comportement : il n'a pas de notion de reliquat à passer. **C'est le plancher qui rend la transplantation nécessaire, pas l'inverse.** +**LM-1203-b — Sous la réserve de stockage, le budget est ANNULÉ, pas réduit.** *(Décision +Patrick, 2026-08-26 ; ne se déduit pas du code.)* + +Quand le niveau de la batterie passe sous sa réserve, le waterfall ne rabote pas le budget : +il le met à **zéro**. Servir « un peu » cumulerait deux inconvénients sans produire le +moindre avantage — cela retarderait la recharge du stockage **et** ne démarrerait +probablement personne, puisque les planchers du §12 mordent d'abord. Un budget résiduel de +300 W devant un premier palier de 500 W ne sert personne ; il ne fait que priver la batterie. + +**Corollaire, et c'est le piège** : une installation dont la réserve est réglée haut ne pilote +plus RIEN sous ce niveau — ni ECS, ni PAC, ni borne. Cela ne produit ni erreur ni panne, donc +rien ne le signale. D'où deux exigences qui accompagnent la règle : + +1. le motif **`BATTERY_RESERVE`** publie le surplus **retenu**, le SOC et le seuil — la cause + se lit en une ligne au lieu de se chercher ; +2. le **défaut d'usine est une réserve, pas une cible**. `0,9` confondait les deux : une cible + est ce qu'on aimerait atteindre, une réserve ce sous quoi on ne descend pas. Abaissé à + `0,2` en 3g-1. + **LM-1204-b — Le scénario d'acceptation de 3g, à monter au banc.** *(Défini avec Patrick, 2026-08-26, quand le banc a reçu une seconde borne.)*