// SPDX-License-Identifier: GPL-3.0-or-later #ifndef INTEGRATIONPLUGINV2C_H #define INTEGRATIONPLUGINV2C_H #include #include #include "extern-plugininfo.h" #include "trydanmodbusmaster.h" #include "trydanmodbustcpmaster.h" /*! * \brief nymea integration plugin for the V2C Trydan EV charger. * * ThingClass \c trydan implementing the \c evcharger interface via Modbus TCP * (Étape 1). All register-level I/O is delegated to TrydanModbusTcpMaster; * this class owns the evcharger business logic: * - ChargeState → pluggedIn / charging mapping * - chargingEnabled = (PauseState==0 AND Lock==0) * - Dynamic optimizer conflict management via PauseDynamic * * No NetworkDeviceMonitor: the Trydan ESP32 WiFi stack saturates under * periodic ICMP pings (latency > 14 s, Modbus timeouts). Connectivity is * derived from Modbus poll success/failure instead. * * Étape 2 (RTU) will add TrydanModbusRtuMaster on the same TrydanModbusMaster * interface; the logic in this file requires no changes. */ class IntegrationPluginV2c : public IntegrationPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginv2c.json") Q_INTERFACES(IntegrationPlugin) public: explicit IntegrationPluginV2c(); void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; public slots: void executeAction(ThingActionInfo *info) override; private: /*! \brief Apply all evcharger states from the last completed poll. */ void updateThingStates(Thing *thing, TrydanModbusMaster *master); /*! * \brief Manage the Dynamic/PauseDynamic conflict on each poll. * * If Dynamic==1 (V2C internal optimizer is running) and HEMS is in control * (chargingEnabled==true), write PauseDynamic=1 to suppress the internal PID * without disabling the telemetry. Release PauseDynamic=0 when HEMS * relinquishes control. Dynamic is NEVER written (would silence ChargePower). * cf. AGENTS.md § CONFLIT and evcc charger/trydan.go. */ void handleDynamicConflict(Thing *thing, TrydanModbusMaster *master); /*! * \brief Construire sessionEnergy en accumulant les incréments de chargeEnergy. * * La Trydan ne publie pas d'énergie de session. Son compteur chargeEnergy * (0x0BC4) est remis à zéro à chaque ARRÊT de charge, câble encore branché — * mesuré le 2026-08-28 : arrêt à t+1 s, remise à zéro à t+8 s, débranchement * seulement à t+127 s. Sur du pilotage par surplus, c'est notre propre arbitrage * qui coupe au passage d'un nuage : le HEMS effacerait sa propre mesure plusieurs * fois par jour, et une recharge de nuit relue au matin vaudrait 0. * * L'accumulation se fait ici, dans le plugin, et pas dans le moteur : le plugin * scrute la borne toutes les 30 s, le moteur à la minute — un incrément effacé * entre deux lectures du moteur lui serait invisible. En sortie, le moteur et * l'app reçoivent le contrat evcharger standard, sans cas particulier V2C en aval. * * \sa m_sessions pour la machine à états, et AGENTS.md § SESSION ENERGY. */ void accumulerEnergieDeSession(Thing *thing, TrydanModbusMaster *master); /*! \brief Restaurer l'état de session persisté (appelé depuis setupThing). */ void restaurerSuiviDeSession(Thing *thing); /*! \brief Persister « une session est ouverte » (écrit sur transition seulement). */ void ecrireSessionOuverte(Thing *thing, bool ouverte); void setDisconnectedState(Thing *thing, const QString &cause = QString()); /*! * \brief Suivi par borne de la session de recharge en cours. * * \c baseline est la dernière valeur brute de chargeEnergy vue par un poll * RÉUSSI ; les polls en échec n'émettent pas updateFinished() et ne touchent * donc à rien — une coupure WiFi gèle le compteur, elle ne l'efface pas. */ struct SuiviDeSession { double baseline = 0.0; ///< dernière valeur brute de chargeEnergy (kWh) bool ouverte = false; ///< un véhicule est branché : session en cours int absences = 0; ///< polls réussis consécutifs lus « débranché » }; /*! * \brief Polls « débranché » consécutifs exigés avant de clore une session. * * Le lien WiFi de la borne est instable (2 à 4 s de latence, 40 % de perte * observés le 2026-08-28 avant stabilisation) : une seule lecture aberrante à * ChargeState=0 effacerait la session. 3 polls x 30 s = 90 s de confirmation. * Marge vérifiée sur le cycle mesuré : débranchement réel à t+127 s, session * suivante à t+278 s — la fenêtre de 90 s tient entre les deux. */ static const int k_absencesPourClore = 3; /*! * \brief Seuil sous lequel une baisse de chargeEnergy est du bruit, pas une RAZ. * * Le compteur firmware monte de façon monotone puis retombe à 0 ; il ne recule * pas de quelques Wh. 1 Wh est très au-dessus de la précision d'un float32 aux * ordres de grandeur en jeu, et très en dessous d'une vraie remise à zéro. */ static constexpr double k_bruitEnergieKWh = 0.001; PluginTimer *m_pluginTimer = nullptr; QHash m_tcpMasters; QHash m_sessions; }; #endif // INTEGRATIONPLUGINV2C_H