diff --git a/debian-qt5/changelog b/debian-qt5/changelog index aaef4d3..0b9bfb4 100644 --- a/debian-qt5/changelog +++ b/debian-qt5/changelog @@ -1,3 +1,15 @@ +powersync-energy-plugin-nymea (1.15.2+etm60) trixie; urgency=medium + + * Dérogation : deux effets de setChargingInfo(), qui déclenche lui-même un + update(). À la pose, l'écriture du mode vient EN DERNIER — le cycle qu'elle + déclenche trouve alors la dérogation déjà posée et la sert immédiatement, au + lieu de la manquer d'un cycle. À la fin, la restitution du mode est DIFFÉRÉE + d'un tour de boucle : les fins sont évaluées depuis update(), et y écrire le + mode imbriquait un update() dans un update() — deux plans et deux dispatchs + pour le même instant, sans qu'aucun verrou de ré-entrance le signale. + + -- Patrick Schurig Tue, 08 Sep 2026 10:52:03 +0200 + powersync-energy-plugin-nymea (1.15.2+etm59) trixie; urgency=medium * Le BOOST, implémenté comme une DÉROGATION et non comme un mode. Elle se diff --git a/docs/DESIGN_DEROGATION.md b/docs/DESIGN_DEROGATION.md index c57fb34..1db9b29 100644 --- a/docs/DESIGN_DEROGATION.md +++ b/docs/DESIGN_DEROGATION.md @@ -295,3 +295,24 @@ sera reprise avec lui. C'est aussi pourquoi la première vérification au banc s (`LM-1012-d`) — c'est précisément ce que le filet de durée couvre. - `ClearLoadDerogation` publie la cause `cancelled`, qui n'est pas une des quatre fins par mécanisme : c'est une fin **par décision**, et la distinguer est le but. + +### Deux effets de `setChargingInfo()` trouvés en préparant l'essai — `+etm60` + +`SmartChargingManager::setChargingInfo()` **déclenche lui-même un `update()`**. Écrire le mode +n'est donc pas un geste neutre, et le lot le touche aux deux bouts. + +**À la pose, l'ordre décide d'une minute.** L'écriture du mode vient **en dernier**, après +l'insertion dans `m_derogations` : le cycle qu'elle déclenche trouve alors la dérogation déjà +posée et la sert immédiatement. L'ordre inverse la lui faisait manquer d'un cycle — soixante +secondes d'attente sur un mécanisme dont la définition est « tout de suite ». + +**À la fin, la restitution est DIFFÉRÉE d'un tour de boucle.** Les fins sont évaluées *depuis* +`update()` ; y appeler `setChargingInfo()` imbriquerait un `update()` dans un `update()` — deux +plans construits et deux dispatchs pour le même instant, sur des charges qu'un seul des deux +croirait avoir commandées. `update()` n'a aucun verrou de ré-entrance, donc rien ne l'aurait +signalé. Le décalage ne se voit nulle part : l'événement de fin part dans la trame du cycle +courant, et le mode est rendu avant le suivant. + +**Effet heureux du retour à `Normal`** : l'amont restaure alors les valeurs manuelles +(`manualChargingEnabled`, `manualMaxChargingCurrent`, `manualDesiredPhaseCount`). La borne +retrouve donc *ses* réglages, et pas seulement son mode. diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp index eee0c4b..f7c3f66 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -1095,18 +1095,19 @@ bool EnergyArbitrator::poseDerogation(const QString &loadId, int dureeS, double // baseWh reste à capturer : au moment de la pose, aucun cycle n'a construit de contexte et // le cumul de session n'est lisible que là (cf. Derogation::baseCapturee). + ChargingInfo pendant; + bool aBasculer = false; if (estBorne) { - const ThingId id(loadId); - const ChargingInfo avant = chargingInfo(id); + const ChargingInfo avant = chargingInfo(ThingId(loadId)); d.previousMode = static_cast(avant.chargingMode()); // Une borne en MANUEL est hors waterfall : le moteur ne la commande pas, et une // dérogation n'y produirait rien du tout. On la fait entrer dans l'arbitrage pour la // durée — et c'est le SEUL cas où le mode est touché. if (avant.chargingMode() == ChargingInfo::ChargingModeNormal) { - ChargingInfo pendant = avant; + pendant = avant; pendant.setChargingMode(ChargingInfo::ChargingModeEco); - setChargingInfo(pendant); d.modeForce = true; + aBasculer = true; } } @@ -1119,6 +1120,13 @@ bool EnergyArbitrator::poseDerogation(const QString &loadId, int dureeS, double : QStringLiteral("sans cible")) .arg(d.previousMode) .arg(d.modeForce ? QStringLiteral(" (forcé en éco pour la durée)") : QString()); + + // L'ÉCRITURE DU MODE VIENT EN DERNIER, et l'ordre est décisif : setChargingInfo() déclenche + // lui-même un cycle. La dérogation est donc DÉJÀ posée quand ce cycle passe, et il la sert + // immédiatement. L'inverse la lui aurait fait manquer d'un cycle — soixante secondes + // d'attente sur un mécanisme dont la définition est « tout de suite ». + if (aBasculer) + setChargingInfo(pendant); return true; } @@ -1138,17 +1146,27 @@ bool EnergyArbitrator::leveDerogation(const QString &loadId) void EnergyArbitrator::rendreModeMemorise(const Derogation &d) { // \rule{LM-1500-d} — RETOUR AU MODE MÉMORISÉ, et à lui seul. Revenir à un défaut sortirait - // une borne manuelle - // de la main de son utilisateur — le contraire exact de ce qu'une dérogation promet, qui - // est de rendre la main. + // une borne manuelle de la main de son utilisateur — le contraire exact de ce qu'une + // dérogation promet, qui est de rendre la main. if (!d.modeForce || d.previousMode < 0) return; const ThingId id(d.loadId); - if (!internalEvChargers().contains(id)) - return; // la borne a disparu : il n'y a plus de réglage à rendre - ChargingInfo info = chargingInfo(id); - info.setChargingMode(static_cast(d.previousMode)); - setChargingInfo(info); + const int mode = d.previousMode; + + // DIFFÉRÉ D'UN TOUR DE BOUCLE, et ce n'est pas une précaution de style : les fins sont + // évaluées DEPUIS update(), et setChargingInfo() déclenche lui-même un update(). Écrire ici + // imbriquerait un cycle dans un cycle — deux plans construits et deux dispatchs pour le + // même instant, sur des charges qu'un seul des deux croirait avoir commandées. + // + // Le décalage ne se voit nulle part : l'événement de fin est publié dans la trame de CE + // cycle, et le mode est rendu avant le suivant. + QTimer::singleShot(0, this, [this, id, mode]() { + if (!internalEvChargers().contains(id)) + return; // la borne a disparu entre-temps : il n'y a plus de réglage à rendre + ChargingInfo info = chargingInfo(id); + info.setChargingMode(static_cast(mode)); + setChargingInfo(info); + }); } void EnergyArbitrator::evaluerDerogations(SurplusContext &ctx, const QDateTime &now)