feat(3g-1): motif BATTERY_RESERVE, et le défaut d'usine ramené de 0,9 à 0,2
Le vrai sujet n'était pas le banc mais le défaut d'usine. Une fois la règle batterie dans le budget, un seuil à 0,9 gèle toute l'installation sous 90 % de SOC — ni ECS, ni PAC, ni borne — sans erreur et sans panne. Personne ne s'en apercevait tant que la règle ne touchait que le VE. 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 ; la règle « sous ce niveau, rien d'autre n'est servi » a une sémantique de réserve et était défaultée comme une cible. 0,2 protège l'autonomie du soir sans confisquer la journée, et un installateur qui veut « la batterie d'abord » monte la valeur délibérément — maintenant que l'écran l'expose. BATTERY_RESERVE publie le surplus RETENU, pas seulement le SOC : « 3 200 W retenus » se comprend là où « batterie à 47 % » se subit. LM-1203-b inscrit la nuance qui ne se déduit pas du code : sous la réserve le budget est annulé, pas réduit. Vérifié au passage, comme demandé : evReservedW devra filtrer sur funding == Grid après la seconde moitié — spot market et échéance restent au proxy, minCurrent et idle disparaissent. Il reste correct tel quel tant que le VE n'est pas dans la cascade. 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
f4e2d3ee22
commit
9157ebdfe7
@ -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 <etm.schurig@gmail.com> 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
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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.)*
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user