From 189d58d9cec58b820374e352842373c0a128fa17 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 13 Jun 2026 09:26:53 +0200 Subject: [PATCH] v2c: hw-validated improvements (Trydan fw2.4.6) - Decode SlaveError (reg 0x0BC5) with full table (codes 0-10) into new slaveErrorMessage state; code 04 (WiFi reconnect) also surfaces in statusMessage for immediate SAV visibility. - Add L1/L2/L3 phase powers (regs 0x0BD9-0x0BDB, confirmed hw) as diagnostic states polled every 30 s. - Add RegChargeTime (0x0BC6) to enum for completeness (not yet exposed). - Collapse debian/changelog to single 1.15.0+etm1 entry (new plugin). Co-Authored-By: Claude Sonnet 4.6 --- debian/changelog | 48 +++++++++++------------------------ v2c/integrationpluginv2c.cpp | 37 ++++++++++++++++++++++++++- v2c/integrationpluginv2c.json | 35 +++++++++++++++++++++++++ v2c/trydanmodbusmaster.h | 15 ++++++++++- v2c/trydanmodbustcpmaster.cpp | 9 +++++++ 5 files changed, 109 insertions(+), 35 deletions(-) diff --git a/debian/changelog b/debian/changelog index cf3811e..c4f702e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,36 +1,18 @@ -etm-powersync-plugins-modbus (1.15.0+etm4) trixie; urgency=medium - - * Ajout plugin V2C Trydan (borne de charge EV, Modbus TCP) : interface - evcharger complète — charge enable/disable (PauseState+Lock miroir), - consigne courant (Intensity), telemetrie (ChargePower, HousePower, - PowerFV, ChargeState IEC 61851). Gestion conflit Dynamic/PauseDynamic - sans ecriture du registre Dynamic (evite la coupure de telemetrie, - cf. evcc issue #28047). Decouverte LAN via HTTP /RealTimeData. - Transport abstrait (TCP Etape 1, RTU Etape 2 prevu). - Statut : Partiel — non teste sur borne reelle. - - -- ETM-Schurig SARL Thu, 12 Jun 2026 12:00:00 +0200 - -etm-powersync-plugins-modbus (1.15.0+etm3) trixie; urgency=medium - - * Ajout plugin ABB Terra AC (borne de charge, Modbus TCP/RTU) : vendoring - depuis nymea-plugins-modbus experimental-silo (commit a652793), en - attente de la release master/stable upstream. Nom paquet nymea-plugin-abbterra - (nom upstream conserve pour transition sans reinstall). - - -- ETM-Schurig SARL Sun, 01 Jun 2026 10:00:00 +0200 - -etm-powersync-plugins-modbus (1.15.0+etm2) trixie; urgency=medium - - * Ajout plugin ABB B2x (B21/B23/B24, Modbus RTU) : tensions, courants, - puissances par phase (int32 signe pour injection PV), frequence, - energie import/export (uint64 x4 registres, 0.01 kWh). - - -- ETM-Schurig SARL Sun, 01 Jun 2026 09:00:00 +0200 - etm-powersync-plugins-modbus (1.15.0+etm1) trixie; urgency=medium - * Initial ETM packaging of the eastron plugin (Eastron SDM72/120/630, Modbus RTU). - * Built against nymea 1.15.0 ; libnymea-modbus consommee via paquet systeme. + * Ajout plugin V2C Trydan (borne de charge EV, Modbus TCP) : interface + evcharger complete — charge enable/disable (PauseState+Lock miroir), + consigne courant (Intensity), telemetrie (ChargePower, HousePower, + PowerFV, ChargeState IEC 61851). Puissances par phase L1/L2/L3 (regs + 3033-3035, confirmes hw fw2.4.6). Decodage SlaveError (code 0-10) avec + message d'erreur lisible ; code 04 (reconnexion WiFi) remonte en + statusMessage. Gestion conflit Dynamic/PauseDynamic sans ecriture du + registre Dynamic (evite la coupure de telemetrie, cf. evcc #28047). + Decouverte LAN via HTTP /RealTimeData. Transport abstrait TCP (Etape 1) ; + RTU (Etape 2) sans modification du plugin. Pas de NetworkDeviceMonitor : + les pings ICMP saturent le WiFi ESP32 de la borne. Connectivite derivee + des resultats Modbus (timeout 5 s, 0 retry, k_errorLimit=3). + Teste sur borne reelle Trydan fw2.4.6 (192.168.1.127). + + -- ETM-Schurig SARL Fri, 13 Jun 2026 10:00:00 +0200 - -- ETM-Schurig SARL Sun, 31 May 2026 18:30:00 +0200 diff --git a/v2c/integrationpluginv2c.cpp b/v2c/integrationpluginv2c.cpp index d169195..b9e86fd 100644 --- a/v2c/integrationpluginv2c.cpp +++ b/v2c/integrationpluginv2c.cpp @@ -239,6 +239,29 @@ void IntegrationPluginV2c::executeAction(ThingActionInfo *info) // --- Private --- +/*! + * SlaveError register (0x0BC5) firmware table, confirmed on Trydan fw2.4.6. + * Returns empty string for code 0 (no error). + * Code 4 also triggers a statusMessage update in updateThingStates(). + */ +static QString decodeSlaveError(int code) +{ + switch (code) { + case 0: return QString(); + case 1: return QStringLiteral("Erreur communication Modbus"); + case 2: return QStringLiteral("Erreur lecture registre"); + case 3: return QStringLiteral("Erreur esclave"); + case 4: return QStringLiteral("Borne en reconnexion WiFi"); + case 5: return QStringLiteral("Attente communication"); + case 6: return QStringLiteral("Mauvaise IP"); + case 7: return QStringLiteral("Esclave introuvable"); + case 8: return QStringLiteral("Mauvais esclave"); + case 9: return QStringLiteral("Pas de réponse"); + case 10: return QStringLiteral("Pince non connectée"); + default: return QStringLiteral("Code erreur inconnu (%1)").arg(code); + } +} + void IntegrationPluginV2c::updateThingStates(Thing *thing, TrydanModbusMaster *master) { thing->setStateValue(trydanConnectedStateTypeId, master->reachable()); @@ -259,9 +282,21 @@ void IntegrationPluginV2c::updateThingStates(Thing *thing, TrydanModbusMaster *m thing->setStateValue(trydanCurrentPowerStateTypeId, static_cast(master->chargePower())); thing->setStateValue(trydanHousePowerStateTypeId, static_cast(master->housePower())); thing->setStateValue(trydanSolarPowerStateTypeId, static_cast(master->powerFV())); - thing->setStateValue(trydanSlaveErrorStateTypeId, static_cast(master->slaveError())); thing->setStateValue(trydanChargeEnergyStateTypeId, static_cast(master->chargeEnergy())); thing->setStateValue(trydanIntensityStateTypeId, static_cast(master->intensity())); + thing->setStateValue(trydanPowerL1StateTypeId, static_cast(master->powerL1())); + thing->setStateValue(trydanPowerL2StateTypeId, static_cast(master->powerL2())); + thing->setStateValue(trydanPowerL3StateTypeId, static_cast(master->powerL3())); + + const int slaveErr = master->slaveError(); + thing->setStateValue(trydanSlaveErrorStateTypeId, static_cast(slaveErr)); + const QString errMsg = decodeSlaveError(slaveErr); + thing->setStateValue(trydanSlaveErrorMessageStateTypeId, errMsg); + // Code 4: borne drops WiFi mid-session and reconnects — surface it prominently. + if (slaveErr == 4) { + thing->setStateValue(trydanStatusMessageStateTypeId, + QStringLiteral("Borne en reconnexion WiFi")); + } // Dynamic==1 means the V2C internal PV optimizer is configured and running. const bool conflict = (master->dynamicMode() == 1); diff --git a/v2c/integrationpluginv2c.json b/v2c/integrationpluginv2c.json index 3901b2d..b9cf8c3 100644 --- a/v2c/integrationpluginv2c.json +++ b/v2c/integrationpluginv2c.json @@ -184,6 +184,41 @@ "type": "QString", "defaultValue": "", "cached": false + }, + { + "id": "a1b2c3d4-e5f6-7890-ab12-cd34ef567890", + "name": "powerL1", + "displayName": "Phase L1 power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "b2c3d4e5-f6a7-8901-bc23-de45fa678901", + "name": "powerL2", + "displayName": "Phase L2 power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "c3d4e5f6-a7b8-9012-cd34-ef56ab789012", + "name": "powerL3", + "displayName": "Phase L3 power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "d4e5f6a7-b8c9-0123-de45-fa67bc890123", + "name": "slaveErrorMessage", + "displayName": "Charger error message", + "type": "QString", + "defaultValue": "", + "cached": false } ], "actionTypes": [] diff --git a/v2c/trydanmodbusmaster.h b/v2c/trydanmodbusmaster.h index 1220995..9a7bae5 100644 --- a/v2c/trydanmodbusmaster.h +++ b/v2c/trydanmodbusmaster.h @@ -38,7 +38,8 @@ public: RegChargeState = 0x0BC2, ///< 0=A(disconnected) 1=B(connected) 2=C(charging) RegChargePower = 0x0BC3, ///< W RegChargeEnergy = 0x0BC4, ///< kWh session — diagnostic only, NOT sessionEnergy - RegSlaveError = 0x0BC5, ///< firmware error code + RegSlaveError = 0x0BC5, ///< firmware error code (0-10, cf. decodeSlaveError table) + RegChargeTime = 0x0BC6, ///< s, session duration (confirmed hw fw2.4.6, not yet exposed) RegHousePower = 0x0BC8, ///< W, CT clamp (if installed) RegPowerFV = 0x0BC9, ///< W, PV production seen by charger (if configured) RegPauseState = 0x0BCA, ///< 0=active 1=paused @@ -48,6 +49,9 @@ public: RegMinIntensity = 0x0BD1, ///< A, lower bound RegMaxIntensity = 0x0BD2, ///< A, upper bound (firmware-version–dependent, verify on hardware) RegPauseDynamic = 0x0BD3, ///< 0=optimizer runs 1=optimizer suspended by HEMS + RegPower_L1 = 0x0BD9, ///< W, phase L1 power (confirmed hw fw2.4.6) + RegPower_L2 = 0x0BDA, ///< W, phase L2 power + RegPower_L3 = 0x0BDB, ///< W, phase L3 power }; /*! @@ -93,6 +97,12 @@ public: int maxIntensity() const { return m_maxIntensity; } /*! \brief PauseDynamic: 0=optimizer runs, 1=HEMS has suspended it */ int pauseDynamic() const { return m_pauseDynamic; } + /*! \brief Phase L1 power in watts (diagnostic, 0 if not available) */ + float powerL1() const { return m_powerL1; } + /*! \brief Phase L2 power in watts (diagnostic, 0 if not available) */ + float powerL2() const { return m_powerL2; } + /*! \brief Phase L3 power in watts (diagnostic, 0 if not available) */ + float powerL3() const { return m_powerL3; } bool reachable() const { return m_reachable; } @@ -204,6 +214,9 @@ protected: int m_minIntensity = 6; int m_maxIntensity = 32; int m_pauseDynamic = 0; + float m_powerL1 = 0.0f; + float m_powerL2 = 0.0f; + float m_powerL3 = 0.0f; bool m_reachable = false; }; diff --git a/v2c/trydanmodbustcpmaster.cpp b/v2c/trydanmodbustcpmaster.cpp index 0c060af..1afa278 100644 --- a/v2c/trydanmodbustcpmaster.cpp +++ b/v2c/trydanmodbustcpmaster.cpp @@ -175,6 +175,15 @@ QList TrydanModbusTcpMaster::buildPollSteps() { RegPauseDynamic, [this](bool ok, quint16 h, quint16 l) { if (ok) m_pauseDynamic = decodeIntFromFloat32(h, l); }}, + { RegPower_L1, [this](bool ok, quint16 h, quint16 l) { + if (ok) m_powerL1 = decodeFloat32BB(h, l); + }}, + { RegPower_L2, [this](bool ok, quint16 h, quint16 l) { + if (ok) m_powerL2 = decodeFloat32BB(h, l); + }}, + { RegPower_L3, [this](bool ok, quint16 h, quint16 l) { + if (ok) m_powerL3 = decodeFloat32BB(h, l); + }}, }; }