fix(v2c): la borne basse du courant lit RegMinIntensity, comme la haute

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) <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-09-08 11:09:36 +02:00
parent 7863517bba
commit d526a1eaae
2 changed files with 58 additions and 5 deletions

15
debian/changelog vendored
View File

@ -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 <etm.schurig@gmail.com> 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.

View File

@ -15,6 +15,39 @@
#include <QDateTime>
#include <QSettings>
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<double>(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<double>(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<double>(master->maxIntensity()));
current = qMin(current, courantMaximalEffectif(master));
const quint16 amps = static_cast<quint16>(qRound(current));
master->writeIntensity(amps);