From d526a1eaae3864422d14f0e708f660b55e9705db Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Tue, 8 Sep 2026 11:09:36 +0200 Subject: [PATCH] fix(v2c): la borne basse du courant lit RegMinIntensity, comme la haute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit executeAction écrêtait le haut avec master->maxIntensity() mais le bas avec un littéral 6.0. Une borne déclarant un minimum de 8 A aurait donc reçu une consigne de 7 A écrite telle quelle : au-dessus du littéral, en dessous de ce que la borne honore. L'asymétrie datait de l'écriture initiale — la lecture du registre 0x0BD1 existait déjà, elle n'était simplement pas utilisée là. Les deux extrémités passent maintenant par courantMinimalEffectif() / courantMaximalEffectif(), qui retiennent la PLUS CONTRAIGNANTE des deux sources : la borne peut être plus exigeante que l'IEC 61851, jamais moins — une lecture aberrante à 0 ne doit pas ouvrir la voie à une consigne de 3 A. La borne haute est refermée sur la basse pour la même raison : RegMaxIntensity dépend de la version de firmware, et un intervalle inversé ferait échouer setStateMinMaxValues. Le même couple sert aux DEUX usages, et c'est le point : ce qui est publié comme min/max du state (lu par le curseur de l'app et par EvCharger::maxChargingCurrentMinValue() côté moteur) et ce qui est écrêté à l'écriture. Les faire diverger donnerait un curseur qui propose une valeur que l'action transforme en pause — une commande sans effet. Co-Authored-By: Claude Opus 5 (1M context) --- debian/changelog | 15 +++++++++++ v2c/integrationpluginv2c.cpp | 48 ++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/debian/changelog b/debian/changelog index 092dba0..8b0fe29 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +etm-powersync-plugins-modbus (1.15.2+etm14) trixie; urgency=medium + + * v2c: the lower current bound now reads RegMinIntensity, like the upper one. + + executeAction clamped the top end with master->maxIntensity() but the bottom + end with a literal 6.0, so a charger declaring an 8 A minimum would have been + sent a 7 A setpoint written as-is. Both ends now go through + courantMinimalEffectif() / courantMaximalEffectif(), which take the stricter + of the charger's declared bound and the IEC 61851 floor -- a misread 0 must + not open the way to a 3 A setpoint. The same pair feeds setStateMinMaxValues, + so the app slider and the action agree; letting them diverge would offer a + value the action turns into a pause. + + -- Patrick Schurig Tue, 08 Sep 2026 11:09:26 +0200 + etm-powersync-plugins-modbus (1.15.2+etm13) trixie; urgency=medium * v2c: sessionEnergy is now BUILT by the plugin, not wired to a register. diff --git a/v2c/integrationpluginv2c.cpp b/v2c/integrationpluginv2c.cpp index c5da9d3..4b86be3 100644 --- a/v2c/integrationpluginv2c.cpp +++ b/v2c/integrationpluginv2c.cpp @@ -15,6 +15,39 @@ #include #include +namespace { + +/*! + * \brief Courant minimal admissible sur cette borne, en ampères. + * + * Le plus contraignant des deux : le minimum IEC 61851 (6 A, sous lequel un véhicule ne + * doit pas se voir proposer de consigne) et celui que la borne DÉCLARE dans RegMinIntensity + * (0x0BD1). La borne gagne quand elle est plus exigeante ; elle ne peut pas descendre sous + * la norme — une lecture aberrante à 0 ne doit pas ouvrir la voie à une consigne de 3 A. + * + * Le même plancher sert à l'écriture (executeAction) et à ce qui est PUBLIÉ comme minimum + * du state (setStateMinMaxValues). Les deux doivent coïncider : un curseur d'app qui + * propose une valeur que l'action transforme en pause serait une commande sans effet. + */ +double courantMinimalEffectif(TrydanModbusMaster *master) +{ + return qMax(6.0, static_cast(master->minIntensity())); +} + +/*! + * \brief Courant maximal admissible, jamais inférieur au minimal. + * + * RegMaxIntensity dépend de la version de firmware et pourrait, mal lu, passer sous le + * plancher. Un intervalle inversé ferait échouer setStateMinMaxValues et rendrait + * l'écrêtage incohérent ; on le referme plutôt sur un point. + */ +double courantMaximalEffectif(TrydanModbusMaster *master) +{ + return qMax(courantMinimalEffectif(master), static_cast(master->maxIntensity())); +} + +} // namespace + IntegrationPluginV2c::IntegrationPluginV2c() {} @@ -129,8 +162,8 @@ void IntegrationPluginV2c::setupThing(ThingSetupInfo *info) thing->setStateValue(trydanConnectedStateTypeId, true); thing->setStateValue(trydanStatusMessageStateTypeId, QString()); thing->setStateMinMaxValues(trydanMaxChargingCurrentStateTypeId, - master->minIntensity(), - master->maxIntensity()); + courantMinimalEffectif(master), + courantMaximalEffectif(master)); // Immediate first poll. For setup the info handler below also calls update(); // the m_updating guard makes the second call a no-op. master->update(); @@ -258,15 +291,20 @@ void IntegrationPluginV2c::executeAction(ThingActionInfo *info) double current = info->action().paramValue( trydanMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble(); - // Clamp to IEC 61851 minimum. Below 6 A → pause rather than send invalid setpoint. - if (current < 6.0) { + // Plancher : le minimum que la BORNE déclare (RegMinIntensity), jamais sous les 6 A + // de l'IEC 61851 — cf. courantMinimalEffectif(). Sous ce plancher → pause, plutôt + // qu'une consigne que la borne n'honorerait pas. La borne haute lisait déjà le + // registre ; la borne basse était un littéral 6.0, donc une borne qui exigerait + // 8 A aurait reçu une consigne de 7 A écrite telle quelle. + const double minA = courantMinimalEffectif(master); + if (current < minA) { master->writePauseState(1); master->writeLock(1); thing->setStateValue(trydanPowerStateTypeId, false); info->finish(Thing::ThingErrorNoError); return; } - current = qMin(current, static_cast(master->maxIntensity())); + current = qMin(current, courantMaximalEffectif(master)); const quint16 amps = static_cast(qRound(current)); master->writeIntensity(amps);