fix(v2c): sessionEnergy construit dans le plugin, et les « puissances » par phase sont des courants

Deux correctifs issus de la même campagne (borne .103, banc .75, phaseCount = 3,
2026-08-28). Ils touchent les mêmes fonctions et sortent des mêmes relevés.

1. sessionEnergy — CONSTRUIT, pas câblé
------------------------------------------------------------------------------
La Trydan ne publie pas d'énergie de session. Son compteur ChargeEnergy (0x0BC4)
est exact — son intégration recoupe currentPower à mieux d'un pour cent, deux
fois de suite — mais il n'est pas conservé : le firmware le remet à zéro à
chaque ARRÊT DE CHARGE, câble encore branché. Cycle mesuré : arrêt à t+1 s,
remise à zéro à t+8 s, débranchement seulement à t+127 s. Le déclencheur est
l'arrêt, pas le débranchement.

C'est notre problème et pas celui du firmware : en pilotage par surplus, c'est
notre propre arbitrage qui coupe au passage d'un nuage. Le HEMS effacerait donc
sa propre mesure plusieurs fois par jour, et une nuit de recharge relue à 7 h
vaudrait 0 — soit « rien livré », ce que la condition C1 de LM-1009 interdit
d'afficher.

accumulerEnergieDeSession() accumule les incréments avant chaque remise à zéro.
Dans le plugin et pas dans le moteur : le plugin scrute à 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 à porter en aval. Même principe que le câblage d'energystorage
sur la batterie SunSpec.

Trois décisions, détaillées dans AGENTS.md § SESSION ENERGY :

- RAZ à l'OUVERTURE de session, jamais à la fermeture. Après le départ de la
  voiture, sessionEnergy garde le total de la session terminée : la question
  « combien la nuit a-t-elle livré ? » se pose au matin, voiture déjà partie.
  Remettre à zéro au débranchement effacerait la réponse au moment précis où on
  la cherche.
- Anti-clignotement STRUCTUREL, pas temporisé. Le débranchement doit être
  confirmé sur 3 polls RÉUSSIS consécutifs. updateFinished() n'est émis qu'après
  une séquence de lecture complète : une coupure WiFi GÈLE le compteur
  d'absences au lieu de le faire avancer. C'est la protection qu'il fallait
  contre le lien de ce matin (latence 2-4 s, 40 % de perte avant stabilisation).
- Persistance sur deux supports. sessionEnergy en state « cached » ; le repère
  de reprise est la dernière valeur brute de chargeEnergy, elle aussi passée en
  « cached » — retirer cette propriété ferait repartir de zéro une recharge qui
  traverse un redémarrage, SANS QUE RIEN NE LE SIGNALE, d'où l'annotation en
  travers du code. Le drapeau « session ouverte » va dans pluginStorage(), écrit
  sur transition seulement (flash de la passerelle).

Deux limites assumées plutôt que contournées :
- Si un débranchement PUIS un rebranchement tiennent entièrement dans une
  coupure, la nouvelle session hérite du total de la précédente. La borne ne
  numérote pas ses sessions, et une RAZ de ChargeEnergy ne lève pas
  l'ambiguïté : elle survient aussi au sein d'une même session. Le choix inverse
  — repartir de zéro à chaque redémarrage — casserait le cas que ce correctif
  existe pour couvrir.
- L'énergie livrée entre le dernier poll et l'arrêt de charge est perdue, bornée
  par un intervalle de poll. NON estimée par currentPower × Δt : on n'invente pas
  une mesure pour combler un trou de mesure.

chargeEnergy reste publié à côté, sous son nom exact — sessionEnergy est la
grandeur construite, chargeEnergy la mesure brute. Si les deux divergent un
jour, on saura laquelle a menti. Seul le displayName change, « Session energy
(diagnostic) » étant devenu mensonger.

Ceci lève l'interdiction précédente (« ne pas exposer sessionEnergy », cf. evcc
issue #28047), mais pas de la façon qu'elle envisageait : elle attendait une
validation de fiabilité, la campagne a séparé exactitude (bonne) et conservation
(nulle).

2. Power_L1/L2/L3 portent des AMPÈRES
------------------------------------------------------------------------------
La feuille V2C intitule 0x0BD9-0x0BDB « Power_Lx ». Mesuré : consigne 6 A →
5,9-6,0 lus ; consigne 10 A → 9,x-10 lus. Sous 230 V une puissance de phase
vaudrait ~1 380 W, pas 5,9. Publiés en Watt, l'app affichait « 5,9 W » là où la
borne dit « 5,9 A » — une unité qui ment, même famille que carBatteryLevel
publié sous un nom de mesure.

States renommés currentL1/L2/L3, unité Ampere. Les UUID ne changent PAS :
l'identifiant a toujours porté des ampères, c'est le nom qui mentait. Les noms
d'enum RegPower_Lx gardent en revanche l'intitulé du constructeur — c'est ainsi
qu'on retrouve l'adresse dans la feuille — avec l'avertissement en commentaire.

Bénéfice de bord : le déséquilibre entre phases, que ne donne aucune autre borne
du catalogue, utile à la protection de surcharge par phase (LM-1006-1).

3. Observations de campagne consignées
------------------------------------------------------------------------------
- Latence de consigne asymétrique, du bon côté pour la sécurité : montée
  6 → 10 A en ~30 s, descente 10 → 6 A en ~7 s.
- maxChargingCurrent (consigne, 0x177D) et intensity (effet, 0x0BCD) diffèrent
  pendant la transition — même couple commandé/réel qu'allocatedW vs mechanism.
  Ne pas « corriger » l'un avec l'autre.
- optimizerSuspended et conflictDetected étaient à True : la borne a son propre
  optimiseur et le HEMS le suspend. Second commandeur potentiel, à lire en
  diagnostic quand un écart consigne/effet n'est pas expliqué par la latence.
- La ThingClass ne déclare que deux actions, maxChargingCurrent et power :
  phaseCount est un réglage de câblage, jamais une commande.

Compile sans avertissement sous -Werror -Wall -Wextra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A41CTzMvir1Y5ABPXW8xpB
This commit is contained in:
Patrick Schurig 2026-08-28 12:29:45 +02:00
parent 1bf0b675c6
commit 0798b2937d
5 changed files with 267 additions and 34 deletions

View File

@ -13,6 +13,7 @@
#include <network/networkdevicediscovery.h>
#include <QDateTime>
#include <QSettings>
IntegrationPluginV2c::IntegrationPluginV2c()
{}
@ -91,6 +92,22 @@ void IntegrationPluginV2c::setupThing(ThingSetupInfo *info)
}
});
// Reprendre le suivi de session AVANT que le premier poll n'arrive : c'est ici,
// et pas au premier poll, que le cache d'états de nymea est encore intact
// (initThing() restaure les états cachés avant setupThing()).
restaurerSuiviDeSession(thing);
// Setup raté : le Thing est détruit par le moteur, et m_sessions garderait une
// clé pendante. Elle n'est jamais déréférencée, mais un Thing réalloué à la même
// adresse hériterait d'un repère qui n'est pas le sien — et d'un sessionEnergy
// qui saute. Notre handler est connecté avant celui du moteur, donc il passe
// avant la destruction.
connect(info, &ThingSetupInfo::finished, this, [this, info]() {
if (info->status() != Thing::ThingErrorNoError) {
m_sessions.remove(info->thing());
}
});
// Reachability is driven by Modbus poll results (not TCP state directly).
// initialize() is now called from TrydanModbusTcpMaster::onConnectionStateChanged(true)
// so we no longer need to call it here — this handler only updates the UI.
@ -168,6 +185,12 @@ void IntegrationPluginV2c::postSetupThing(Thing *thing)
void IntegrationPluginV2c::thingRemoved(Thing *thing)
{
delete m_tcpMasters.take(thing);
m_sessions.remove(thing);
// Le drapeau de session persisté n'a plus de propriétaire : le laisser traîner
// ferait repartir une borne réappairée sur une session qui n'existe plus.
QSettings *stockage = pluginStorage();
stockage->remove(thing->id().toString());
if (myThings().isEmpty() && m_pluginTimer) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
@ -310,11 +333,22 @@ void IntegrationPluginV2c::updateThingStates(Thing *thing, TrydanModbusMaster *m
thing->setStateValue(trydanCurrentPowerStateTypeId, static_cast<double>(master->chargePower()));
thing->setStateValue(trydanHousePowerStateTypeId, static_cast<double>(master->housePower()));
thing->setStateValue(trydanSolarPowerStateTypeId, static_cast<double>(master->powerFV()));
thing->setStateValue(trydanChargeEnergyStateTypeId, static_cast<double>(master->chargeEnergy()));
thing->setStateValue(trydanIntensityStateTypeId, static_cast<double>(master->intensity()));
thing->setStateValue(trydanPowerL1StateTypeId, static_cast<double>(master->powerL1()));
thing->setStateValue(trydanPowerL2StateTypeId, static_cast<double>(master->powerL2()));
thing->setStateValue(trydanPowerL3StateTypeId, static_cast<double>(master->powerL3()));
// currentL1/L2/L3 : ces registres sont intitulés « Power_Lx » par V2C mais portent
// des ampères (cf. TrydanModbusMaster::RegPower_L1). Publiés en A, sous un nom de
// courant : une unité qui ment coûte plus cher qu'un state manquant.
thing->setStateValue(trydanCurrentL1StateTypeId, static_cast<double>(master->currentL1()));
thing->setStateValue(trydanCurrentL2StateTypeId, static_cast<double>(master->currentL2()));
thing->setStateValue(trydanCurrentL3StateTypeId, static_cast<double>(master->currentL3()));
// sessionEnergy AVANT chargeEnergy : l'accumulateur travaille sur la valeur
// fraîche du master, mais l'ordre garde les deux states cohérents pour qui les
// lit à la volée. chargeEnergy reste publié sous son nom exact, à côté : c'est la
// mesure brute de la borne, sessionEnergy la grandeur construite. Si les deux
// divergent un jour, on saura laquelle a menti.
accumulerEnergieDeSession(thing, master);
thing->setStateValue(trydanChargeEnergyStateTypeId, static_cast<double>(master->chargeEnergy()));
const int slaveErr = master->slaveError();
thing->setStateValue(trydanSlaveErrorStateTypeId, static_cast<uint>(slaveErr));
@ -374,6 +408,122 @@ void IntegrationPluginV2c::handleDynamicConflict(Thing *thing, TrydanModbusMaste
}
}
void IntegrationPluginV2c::accumulerEnergieDeSession(Thing *thing, TrydanModbusMaster *master)
{
SuiviDeSession &suivi = m_sessions[thing];
const double brut = static_cast<double>(master->chargeEnergy());
const bool branche = (master->chargeState() >= 1);
// 1. Confirmation du débranchement. Seuls les polls RÉUSSIS parviennent ici :
// updateFinished() n'est émis qu'après une séquence de lecture complète. Une
// coupure réseau gèle donc le compteur d'absences, elle ne le fait pas avancer.
suivi.absences = branche ? 0 : suivi.absences + 1;
// 2. Clôture, une fois le débranchement confirmé sur k_absencesPourClore polls.
// On ne remet PAS sessionEnergy à zéro ici : la question « combien la nuit
// a-t-elle livré ? » se pose au matin, voiture déjà partie. La valeur reste
// affichée jusqu'au branchement suivant, qui seul ouvre une nouvelle session.
if (suivi.ouverte && suivi.absences >= k_absencesPourClore) {
suivi.ouverte = false;
ecrireSessionOuverte(thing, false);
qCDebug(dcV2C()) << thing->name() << ": session close a"
<< thing->stateValue(trydanSessionEnergyStateTypeId).toDouble()
<< "kWh (debranchement confirme sur" << k_absencesPourClore << "polls)";
}
// 3. Ouverture : un véhicule vient d'être branché. On part de la valeur brute et
// non de zéro — si le branchement a eu lieu pendant une coupure réseau, la
// borne a déjà compté, et cette énergie appartient à la session qui s'ouvre.
if (!suivi.ouverte && branche) {
suivi.ouverte = true;
suivi.baseline = brut;
thing->setStateValue(trydanSessionEnergyStateTypeId, brut);
ecrireSessionOuverte(thing, true);
qCDebug(dcV2C()) << thing->name() << ": nouvelle session, depart a" << brut << "kWh";
return;
}
// 4. Session close et véhicule absent : rien à accumuler. On suit quand même la
// valeur brute pour que la prochaine ouverture parte du bon repère.
if (!suivi.ouverte) {
suivi.baseline = brut;
return;
}
// 5. Session ouverte : accumuler l'écart depuis le dernier poll réussi.
double session = thing->stateValue(trydanSessionEnergyStateTypeId).toDouble();
const double ecart = brut - suivi.baseline;
if (ecart >= 0.0) {
// Cas courant : le compteur de la borne monte, on le suit.
session += ecart;
} else if (ecart > -k_bruitEnergieKWh) {
// Recul infime : bruit de lecture, pas une remise à zéro. On n'ajoute rien ;
// la baseline suit tout de même la valeur lue, donc la remontée qui suivra
// ne sera pas comptée deux fois.
} else {
// Remise à zéro par la borne (arrêt de charge, câble encore branché). Les
// incréments d'avant la RAZ sont déjà dans session ; ce qui reste à ajouter
// est ce que la borne a compté DEPUIS. Perte résiduelle : l'énergie livrée
// entre le dernier poll et l'arrêt, bornée par un intervalle de poll — c'est
// exactement pourquoi ce calcul vit dans le plugin (30 s) et pas dans le
// moteur (à la minute), qui raterait des cycles entiers.
session += brut;
qCDebug(dcV2C()) << thing->name() << ": chargeEnergy remis a zero par la borne ("
<< suivi.baseline << "->" << brut << "kWh), session cumulee"
<< session << "kWh";
}
suivi.baseline = brut;
thing->setStateValue(trydanSessionEnergyStateTypeId, session);
}
void IntegrationPluginV2c::restaurerSuiviDeSession(Thing *thing)
{
SuiviDeSession suivi;
// Repère de reprise = la dernière valeur BRUTE publiée avant l'arrêt de nymead.
// Elle est relue du cache d'états : d'où "cached": true sur chargeEnergy dans
// integrationpluginv2c.json. NE PAS retirer cette propriété — l'accumulateur en
// dépend, et sans elle une recharge qui traverse un redémarrage repartirait de
// zéro sans que rien ne le signale.
suivi.baseline = thing->stateValue(trydanChargeEnergyStateTypeId).toDouble();
QSettings *stockage = pluginStorage();
stockage->beginGroup(thing->id().toString());
suivi.ouverte = stockage->value("sessionOuverte", false).toBool();
stockage->endGroup();
m_sessions.insert(thing, suivi);
// Ce que fera le premier poll, selon l'état retrouvé :
// - session ouverte + véhicule toujours branché → l'écart accumulé pendant la
// coupure est repris (cas du redémarrage de nymead en pleine recharge) ;
// - session ouverte + véhicule absent → clôture après confirmation, la valeur
// restaurée reste lisible ;
// - session close + véhicule branché → nouvelle session, remise à zéro.
// LIMITE ASSUMÉE : si la voiture a été débranchée PUIS rebranchée pendant la
// coupure, on ne peut pas le savoir — la borne ne numérote pas ses sessions — et
// la nouvelle session héritera du total de la précédente. Le choix inverse
// (repartir de zéro à chaque redémarrage) casserait justement le cas que ce
// correctif existe pour couvrir : la recharge de nuit relue au matin.
qCDebug(dcV2C()) << thing->name() << ": reprise du suivi de session — ouverte ="
<< suivi.ouverte << ", repere chargeEnergy =" << suivi.baseline
<< "kWh, sessionEnergy restauree ="
<< thing->stateValue(trydanSessionEnergyStateTypeId).toDouble() << "kWh";
}
void IntegrationPluginV2c::ecrireSessionOuverte(Thing *thing, bool ouverte)
{
// Écrit sur transition seulement (ouverture / clôture), jamais à chaque poll :
// le stockage est sur la mémoire flash de la passerelle.
QSettings *stockage = pluginStorage();
stockage->beginGroup(thing->id().toString());
stockage->setValue("sessionOuverte", ouverte);
stockage->endGroup();
}
void IntegrationPluginV2c::setDisconnectedState(Thing *thing, const QString &cause)
{
thing->setStateValue(trydanConnectedStateTypeId, false);

View File

@ -60,11 +60,69 @@ private:
*/
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<Thing *, TrydanModbusTcpMaster *> m_tcpMasters;
QHash<Thing *, SuiviDeSession> m_sessions;
};
#endif // INTEGRATIONPLUGINV2C_H

View File

@ -168,12 +168,23 @@
"defaultValue": 0
},
{
"id": "fe616e45-39cb-4be3-8426-9d27c7a64f06",
"name": "chargeEnergy",
"displayName": "Session energy (diagnostic)",
"id": "5b071c75-5e47-406e-ba5d-5c60d5bdd879",
"name": "sessionEnergy",
"displayName": "Session energy",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
"defaultValue": 0,
"cached": true,
"suggestLogging": true
},
{
"id": "fe616e45-39cb-4be3-8426-9d27c7a64f06",
"name": "chargeEnergy",
"displayName": "Charge energy (raw charger counter)",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0,
"cached": true
},
{
"id": "71773194-ebd3-4ded-893c-14bfef9b1854",
@ -208,28 +219,28 @@
},
{
"id": "a1b2c3d4-e5f6-7890-ab12-cd34ef567890",
"name": "powerL1",
"displayName": "Phase L1 power",
"name": "currentL1",
"displayName": "Phase L1 current",
"type": "double",
"unit": "Watt",
"unit": "Ampere",
"defaultValue": 0,
"cached": false
},
{
"id": "b2c3d4e5-f6a7-8901-bc23-de45fa678901",
"name": "powerL2",
"displayName": "Phase L2 power",
"name": "currentL2",
"displayName": "Phase L2 current",
"type": "double",
"unit": "Watt",
"unit": "Ampere",
"defaultValue": 0,
"cached": false
},
{
"id": "c3d4e5f6-a7b8-9012-cd34-ef56ab789012",
"name": "powerL3",
"displayName": "Phase L3 power",
"name": "currentL3",
"displayName": "Phase L3 current",
"type": "double",
"unit": "Watt",
"unit": "Ampere",
"defaultValue": 0,
"cached": false
},

View File

@ -37,7 +37,7 @@ public:
enum ReadRegister : quint16 {
RegChargeState = 0x0BC2, ///< 0=A(disconnected) 1=B(connected) 2=C(charging)
RegChargePower = 0x0BC3, ///< W
RegChargeEnergy = 0x0BC4, ///< kWh session — diagnostic only, NOT sessionEnergy
RegChargeEnergy = 0x0BC4, ///< kWh, remis à zéro à chaque ARRÊT de charge — source de sessionEnergy, pas sessionEnergy
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)
@ -49,9 +49,15 @@ 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
// \warning La feuille V2C intitule ces registres « Power_L1/L2/L3 », mais ils
// portent des AMPÈRES, pas des watts. Mesuré sur borne le 2026-08-28 :
// consigne 6 A → 5,9-6,0 lus ; consigne 10 A → 9,x-10 lus. Une puissance de
// phase vaudrait ~1 380 W sous 230 V, pas 5,9. Le nom d'enum garde l'intitulé
// du constructeur (c'est ainsi qu'on retrouve l'adresse dans la feuille) ; les
// accesseurs et les states publient la grandeur réelle : currentL1/L2/L3, en A.
RegPower_L1 = 0x0BD9, ///< A, phase L1 current (confirmed hw fw2.4.6)
RegPower_L2 = 0x0BDA, ///< A, phase L2 current
RegPower_L3 = 0x0BDB, ///< A, phase L3 current
};
/*!
@ -75,7 +81,15 @@ public:
int chargeState() const { return m_chargeState; }
/*! \brief ChargePower in watts */
float chargePower() const { return m_chargePower; }
/*! \brief Session energy in kWh (diagnostic only — firmware reliability unvalidated) */
/*!
* \brief Compteur d'énergie brut de la borne, en kWh.
*
* \warning Ce n'est PAS l'énergie de session : le firmware le remet à zéro à
* chaque ARRÊT de charge, câble toujours branché (mesuré le 2026-08-28 : remise
* à zéro 8 s après l'arrêt, débranchement seulement 119 s plus tard). Le plugin
* en accumule les incréments pour construire sessionEnergy — cf.
* IntegrationPluginV2c::accumulerEnergieDeSession().
*/
float chargeEnergy() const { return m_chargeEnergy; }
/*! \brief Firmware error code */
int slaveError() const { return m_slaveError; }
@ -97,12 +111,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; }
/*! \brief Phase L1 current in amperes (diagnostic, 0 if not available) */
float currentL1() const { return m_currentL1; }
/*! \brief Phase L2 current in amperes (diagnostic, 0 if not available) */
float currentL2() const { return m_currentL2; }
/*! \brief Phase L3 current in amperes (diagnostic, 0 if not available) */
float currentL3() const { return m_currentL3; }
bool reachable() const { return m_reachable; }
@ -214,9 +228,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;
float m_currentL1 = 0.0f;
float m_currentL2 = 0.0f;
float m_currentL3 = 0.0f;
bool m_reachable = false;
};

View File

@ -191,13 +191,13 @@ QList<TrydanModbusTcpMaster::ReadStep> TrydanModbusTcpMaster::buildPollSteps()
if (ok) m_pauseDynamic = decodeIntFromFloat32(h, l);
}},
{ RegPower_L1, [this](bool ok, quint16 h, quint16 l) {
if (ok) m_powerL1 = decodeFloat32BB(h, l);
if (ok) m_currentL1 = decodeFloat32BB(h, l);
}},
{ RegPower_L2, [this](bool ok, quint16 h, quint16 l) {
if (ok) m_powerL2 = decodeFloat32BB(h, l);
if (ok) m_currentL2 = decodeFloat32BB(h, l);
}},
{ RegPower_L3, [this](bool ok, quint16 h, quint16 l) {
if (ok) m_powerL3 = decodeFloat32BB(h, l);
if (ok) m_currentL3 = decodeFloat32BB(h, l);
}},
};
}