La classe trydan ne déclarait qu'evcharger + connectable. Ça la tenait hors de
EnergyManagerImpl::watchThing(), qui ne branche un Thing sur le journal
d'énergie que s'il déclare energymeter, smartmeterconsumer, smartmeterproducer
ou energystorage. Energy.GetThingPowerLogs ne REJETAIT pas la demande : il
rendait zéro ligne pour ce thingId, et l'app listait la borne en « sans
historique » alors que sa puissance instantanée était parfaitement lisible.
Deux bornes côte à côte, une seule avec un passé.
Au passage, le portier n'est pas `smartmeter` — cette interface est VIDE, aucun
state requis, la déclarer n'aurait rien produit. C'est `smartmeterconsumer` qui
compte, et elle exige totalEnergyConsumed.
Or la borne n'a pas de totalisateur : aucun registre de la carte Modbus V2C n'en
porte un, et ChargeEnergy (0x0BC4) est effacé à chaque arrêt de charge. Il est
donc CONSTRUIT, comme sessionEnergy, sur les mêmes incréments — et désormais par
la même règle unique, incrementEnergie(), partagée par les deux compteurs pour
qu'une correction ne puisse jamais s'appliquer à un seul. Contrairement à
sessionEnergy il ne se remet jamais à zéro et n'a aucune condition de session :
une recharge lancée à la main sur la borne, hors pilotage, y entre aussi.
Deux repères distincts (baseline / baselineTotale) et non un seul : la machine à
états de session sort par plusieurs chemins qui écrivent baseline sans
accumuler, un repère partagé ferait dépendre le total de l'ORDRE des deux
appels.
CE QUI A LEVÉ LE BLOCAGE « MONOTONE À VIE »
Le brief tenait ce totalisateur pour bloqué : la moindre perte se lirait comme
une consommation négative en aval. C'est trop strict, et vérifié : le moteur
tient son PROPRE compteur, se cale sur la première valeur sans l'ajouter à son
diff, et resynchronise explicitement quand le compteur d'un Thing recule
(« seems to have been reset »). Une perte côté plugin ne peut ni injecter de
négatif en aval, ni effacer le journal déjà écrit, ni produire une marche à
l'ajout du Thing. L'exigence réelle est « ne jamais reculer PENDANT une
exécution » — ce que le state "cached": true et le repère relu de chargeEnergy
au setup assurent.
Limite assumée, la même que sessionEnergy : l'énergie livrée entre le dernier
poll réussi et une RAZ de la borne est perdue, bornée par un intervalle de poll
(30 s). C'est pourquoi ce calcul vit dans le plugin et pas dans le moteur.
VÉRIFIÉ SUR LE BANC (.75, 1.15.2+etm14, 2026-09-08 10:57)
energylogs.sqlite, pour {4d1adafd-cafc-4813-93b1-ca8933ce71ab} :
- thingCache : ligne absente avant, créée après — c'est watchThing() ;
- thingPower : 0 ligne avant, puis une par minute comme les cinq autres Things.
À SAVOIR : sur un Thing déjà appairé, things.conf écrase le `logged: true` que
l'interface porte pour currentPower/totalEnergyConsumed (loggedStateTypeIds y
vaut déjà {connected}, {charging}, {power}). Le journal d'énergie du moteur ne
dépend PAS de ce réglage — la courbe marche sans y toucher.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
The Trydan ESP32 WiFi stack saturates under nymea's periodic ICMP pings
(observed: latency > 14 s, Modbus timeouts, charger drops off).
Changes:
- NetworkDeviceMonitor removed entirely from setupThing() and thingRemoved().
The charger is expected to have a DHCP-reserved IP (documented in code).
Address comes from trydanThingAddressParamTypeId param — already the correct
path after the previous setup fix.
- Connectivity now derives from Modbus poll success/failure only (reachable()
from TrydanModbusTcpMaster), NOT from network ping. A ping response does
not imply Modbus usability on this hardware.
- postSetupThing() timer (30 s): if not reachable, calls connectDevice() for
reconnect; if reachable, calls update(). The 30 s period IS the backoff —
no aggressive retry loop.
- Modbus timeout: 2 s → 5 s (tolerates residual WiFi latency spikes).
- Modbus retries per read: 1 → 0 (abort fast on first timeout; full poll
sequence already aborts at first error via doNextRead).
- k_errorLimit: 5 → 3 (3 × 5 s = 15 s before marking unreachable).
- New state "statusMessage" (QString): set on disconnect with timestamp +
cause ("timeout Modbus — vérifier signal WiFi de la borne"); cleared on
successful poll. Visible in nymea-app; helps SAV without SSH access.
- "networkdevice" removed from JSON interfaces (contract broken without monitor).
Invariants preserved: Big/Big float32 decode, no block read, PauseState+Lock
mirror, PauseDynamic conflict management, writeCompleted address filtering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Étape 1 complete: V2C Trydan nymea plugin (Modbus TCP, status Partial).
Plugin (IntegrationPluginV2c):
- discoverThings: delegates to V2cTcpDiscovery, maps results to
ThingDescriptors; createMethod "user" bypasses discovery.
- setupThing: registers a NetworkDeviceMonitor (IP tracking on DHCP
renewal), creates TrydanModbusTcpMaster, wires reachableChanged →
initialize() → state update on initializationFinished(), then starts
periodic polls on a 30s PluginTimer.
- updateThingStates: ChargeState 0/1/2 → pluggedIn/charging;
power = (PauseState==0 AND Lock==0) per evcc trydan.go Enabled().
- handleDynamicConflict: if Dynamic==1 (V2C internal PV optimizer),
write PauseDynamic=1 to suppress it. NEVER write Dynamic=0 (silences
ChargePower telemetry). Writes only on state transition to minimize
flash wear. cf. github.com/evcc-io/evcc/issues/28047.
- Action "power": writes PauseState then Lock (always in mirror), then
fire-and-forgets PauseDynamic if Dynamic==1. Action handlers filter
writeCompleted by register address to avoid reacting to concurrent
background writes.
- Action "maxChargingCurrent": clamps to [minIntensity, maxIntensity],
writes Intensity. Below 6A → pause instead of invalid setpoint.
JSON (integrationpluginv2c.json):
- ThingClass "trydan" implementing evcharger + connectable + networkdevice.
- settingsTypes: suspendInternalOptimizer (bool, default true) — allows
the user to disable Dynamic conflict management if they want the V2C
PID to remain active alongside the HEMS.
- State "chargeEnergy" exposed as diagnostic only (NOT sessionEnergy) —
firmware reliability on this register is unvalidated; cf. evcc #28047.
Build (v2c.pro):
- MODBUS_CONNECTIONS intentionally empty: the modbus-tool code generator
assumes non-overlapping block reads; leaving it empty skips generation
while still linking nymea-modbus via the PKGCONFIG in modbus.pri.
VendorId 56c3e7bb… is new (no existing V2C vendor in the repo).
PluginId f0692725… is new (generated for this plugin).
PORTING_STATUS_modbus.md documents register sources, the 4 items that
require validation on real hardware, and the "no beta matrix" constraint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>