diff --git a/debian-qt5/changelog b/debian-qt5/changelog index 4745ee4..3d38acc 100644 --- a/debian-qt5/changelog +++ b/debian-qt5/changelog @@ -1,3 +1,24 @@ +powersync-energy-plugin-nymea (1.15.2+etm11) trixie; urgency=medium + + * ECS-309-b — le motif ne nomme un verrou que si ce verrou a déplacé la consigne. Le + critère était « consigne > budget », vrai dès que le budget passe sous zéro à palier + nul : 0 > -389. Le motif annonçait alors « Verrou minOn — chauffe-eau maintenue à 0 W + (puissance engagée, budget -389 W) » — un verrou qui ne mordait pas, sur une puissance + engagée nulle, quand le soutirage était la seule cause. Le critère compare désormais la + consigne appliquée à la consigne VOULUE avant écrêtage : relevée → minOn, rabaissée → + minOff, inchangée → le budget. Les deux branches se lisent sur le même axe. + * Constaté au banc le 2026-08-13 sur +etm10, sept fois en quatre heures, toujours au + cycle suivant un palier 0. Le défaut est antérieur à ECS-309, qui a ajouté la branche + minOff après lui sans voir que la première capturait déjà un cas étranger. + * Il ne s'agissait pas que de lisibilité. La branche minOn étant évaluée en premier, ce + défaut masquait le motif d'armement à froid d'ECS-412 chaque fois que le budget était + négatif au redémarrage — soit au fond du creux, précisément l'instant où il faut + redémarrer pour observer minOff. La campagne aurait mesuré le mauvais verrou. + * Suite complète : 112 tests, 0 échec. Le cas est vérifié échouant sans le correctif, + où il reproduit le texte exact relevé sur la machine. + + -- Patrick Schurig Thu, 13 Aug 2026 08:00:00 +0200 + powersync-energy-plugin-nymea (1.15.2+etm10) trixie; urgency=medium * ECS-309 — motif de décision fidèle au mécanisme. Lorsqu'un verrou, et non le budget, diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.cpp b/energyplugin/etm/scheduler/rulebasedscheduler.cpp index 87b3186..6989fe6 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.cpp +++ b/energyplugin/etm/scheduler/rulebasedscheduler.cpp @@ -254,7 +254,14 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc, // vrai. Un motif faux envoie diagnostiquer le mauvais problème, avec l'autorité d'une // explication — constaté au banc le 2026-08-09 : palier 0 avec 7706 W disponibles, et // « Surplus insuffisant (7706 W) » publié. - if (setpointW > budgetW) + // ECS-309-b — le critère est ce que le verrou a FAIT à la consigne voulue, pas une + // comparaison au budget. « setpointW > budgetW » nommait minOn dès que le budget était + // négatif à palier nul : 0 > −389 est vrai, et le motif annonçait « maintenue à 0 W + // (puissance engagée) » alors que rien n'était engagé et que le soutirage était la seule + // cause. Constaté au banc le 2026-08-13, sept fois en quatre heures, toujours au cycle + // suivant un palier 0. Les deux branches se lisent maintenant sur le même axe : + // le verrou a relevé la consigne (minOn), ou il l'a rabaissée (minOff). + if (setpointW > vouluW) la.reason = QStringLiteral("Verrou minOn — %1 maintenue à %2 W (puissance engagée, " "budget %3 W) ; le résidu en tient compte") .arg(lc.label).arg(qRound(setpointW)).arg(qRound(budgetW)); diff --git a/specs/spec_ecs.md b/specs/spec_ecs.md index 0063e2f..823a839 100644 --- a/specs/spec_ecs.md +++ b/specs/spec_ecs.md @@ -248,6 +248,30 @@ Trois cas DOIVENT être distinguables à la lecture du texte : | verrou `minOn` maintenant un palier **supérieur** au budget | le verrou `minOn`, et le palier qu'il retient | | verrou `minOff` empêchant l'enclenchement | le verrou `minOff`, et l'interdiction de monter | +**ECS-309-b — Le critère est l'effet du verrou sur la consigne, pas une comparaison +au budget.** Le motif NE DOIT nommer un verrou que lorsque ce verrou a effectivement +déplacé la consigne. Il DOIT donc se déduire de la comparaison entre la consigne +appliquée et la consigne **voulue** avant écrêtage : relevée → `minOn`, rabaissée → +`minOff`, inchangée → le budget. Comparer la consigne au **budget** ne distingue pas +ces cas. + +> Défaut constaté au banc le 2026-08-13, sur `1.15.2+etm10`, **sept fois en quatre +> heures**, systématiquement au cycle suivant un palier 0 : `« Verrou minOn — +> chauffe-eau maintenue à 0 W (puissance engagée, budget -389 W) »`. Le test +> `setpointW > budgetW` est vrai dès que le budget est négatif à palier nul — `0 > +> -389` — et le motif annonçait alors un verrou qui ne mordait pas, sur une +> « puissance engagée » de 0 W, quand le soutirage était la seule cause. +> +> Ce critère est **antérieur à ECS-309**, qui ne l'a pas introduit mais ne l'a pas +> audité non plus : ECS-309 a ajouté la branche `minOff` *après* lui, sans voir que +> la première branche capturait déjà un cas qui ne lui appartenait pas. +> +> **Conséquence sur le protocole de mesure.** La branche `minOn` étant évaluée avant +> celle de `minOff`, ce défaut masquait le motif d'armement à froid d'ECS-412 chaque +> fois que le budget était négatif au moment du redémarrage — c'est-à-dire au fond du +> creux, précisément là où il faut redémarrer pour observer `minOff`. Il fallait donc +> le corriger **avant** la campagne, non après. + **Cas observé au banc (2026-08-09)**, pendant la fenêtre d'armement à froid d'ECS-412 : palier plafonné à 0 avec **7706 W de surplus disponible**, et motif publié « Surplus insuffisant (7706 W) ». La décision était juste, le motif disait @@ -695,6 +719,7 @@ mécanique reste **L0**. Perte de sonde → mode sans sonde, pas arrêt d'urgenc |---|---|---| | ECS-306 | simulation | `testEcsBudgetUnderLock` | | ECS-309 | simulation | `testEcsColdStartLockExpires` — les trois causes distinguées par le TEXTE du motif (minOff / budget / minOn), vérifié échouant sans le correctif | +| ECS-309-b | simulation | `testEcsColdStartLockExpires` §4 — palier 0 + budget négatif SANS verrou actif : le motif nomme le budget, jamais `minOn`. Vérifié échouant sans le correctif, où il reproduit le texte exact du banc | | ECS-412 | simulation | `testEcsRebuildPreservesLock` + **`testEcsColdStartLockExpires`** (armement à froid transitoire) | | ECS-303 | banc | relevé de commutations à la frontière de recombinaison (§4.0) — **mesurer avant de coder** | | ECS-305 | unitaire | `testEcsSwitchCount` — compteur par relais | @@ -777,6 +802,9 @@ protocole dans le même lot.** | 2026-08-08 | **Étape 1 OUVERTE** — trois exigences : ECS-306, ECS-411, ECS-412 | | 2026-08-08 | ECS-412 : au démarrage à froid, le verrou est **ARMÉ** (défaut sûr), jamais purgé | | 2026-08-09 | ECS-412 précisé : cet armement est **TRANSITOIRE** et posé paresseusement au premier `now`. Défaut de blocage circulaire constaté **au banc**, corrigé, couvert par `testEcsColdStartLockExpires` | +| 2026-08-13 | **ECS-309-b créé et FAIT** — le critère du motif est l'effet du verrou sur la consigne voulue, pas une comparaison au budget. `setpointW > budgetW` nommait `minOn` à palier nul dès que le budget était négatif ; constaté au banc sept fois en quatre heures sur `+etm10`. Antérieur à ECS-309, non audité par lui. Masquait le motif d'armement à froid d'ECS-412 au fond du creux — donc corrigé avant la campagne du volet 2, pas après | +| 2026-08-13 | **Journal du banc `.75` rendu persistant** — Raspberry Pi OS force `Storage=volatile` (`40-rpi-volatile-storage.conf`) : le journal vivait en RAM, plafonné à 8,3 Mo, et la fenêtre d'armement à froid du 12 août avait été rotée avant d'être lue. Drop-in `99-etm-retention.conf` (200 Mo, 1 mois) + `journalctl --flush`, et `ThingManager.debug` coupé — 137 → 25 lignes/min, soit ~6,5 Mo/jour sur la carte SD | +| 2026-08-13 | **ECS-411 vérifié sur le chemin réel** — au redémarrage de 06:15, `« [RelayRouter] chauffe-eau — palier repris au démarrage: 2500 W »`. La relecture des relais au démarrage fonctionne sur la machine, pas seulement en simulation | | 2026-08-11 | **ECS-309 créé et FAIT** — motif fidèle au mécanisme. Le chemin minOff attribuait l'issue au budget ; le motif nomme désormais le verrou et cite le budget DISPONIBLE pour couper court à l'explication budgétaire. Les cas minOn et `minStateHold` étaient déjà fidèles | | 2026-08-10 | **Va-et-vient RPC cassé par LM-300** — `GetLoadConfig` sérialise via le méta-objet, donc un `sgReady` VIDE sur toute charge non SG-Ready ; le schéma SET exigeait `states` dès la clé présente, et nymea rejetait avant le handler. Constaté au banc à la première tentative réelle de `SetLoadConfig` : aucun test ne rejouait le geste de l'app (lire, modifier, réécrire). Corrigé en `o:states` — c'est le mur documenté en LM-302, la validation réelle restant dans `isValid()` | | 2026-08-09 | **ECS-110-b créé et FAIT** — un Thing n'appartient qu'à une charge active. Trou trouvé en répondant à la question « le cas général est-il couvert ? » : il ne l'était pas du tout. `LoadConfigStore::validateSet()` ; doublons intra-charge également refusés | diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index 690a207..a0f8721 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -1189,6 +1189,20 @@ void Simulation::testEcsColdStartLockExpires() const QString mOn = motifPour(r4, -400, t0.addSecs(10)); // import 400 W → budget 600 < 1000 QVERIFY2(mOn.contains("minOn"), qUtf8Printable("motif: " + mOn)); + // 4. [ECS-309-b] Palier 0 et budget NÉGATIF, sans aucun verrou actif. Le soutirage est + // la seule cause ; nommer minOn ici est un mensonge à double titre — aucun verrou ne + // mord, et « puissance engagée » désigne 0 W. Le critère « setpointW > budgetW » + // l'affirmait pourtant, 0 > −389 étant vrai. Constaté au banc le 2026-08-13, sept + // fois en quatre heures, toujours au cycle suivant un palier 0. + // Sans le correctif ce cas produit le texte exact du banc : + // « Verrou minOn — ... maintenue à 0 W (puissance engagée, budget -389 W) ». + const QString mSoutirage = motifPour(r3, -389, t0); + QVERIFY2(mSoutirage.contains("insuffisant", Qt::CaseInsensitive), + qUtf8Printable("motif: " + mSoutirage)); + QVERIFY2(!mSoutirage.contains("Verrou"), qUtf8Printable("motif: " + mSoutirage)); + QVERIFY2(!mSoutirage.contains("puissance engagée"), qUtf8Printable("motif: " + mSoutirage)); + QVERIFY2(mSoutirage.contains("-389"), qUtf8Printable("motif: " + mSoutirage)); + // Les trois motifs sont distincts : c'est la lisibilité qu'ECS-309 exige. QVERIFY(mOff != mBudget && mBudget != mOn && mOff != mOn);