5 Commits

Author SHA1 Message Date
Patrick Schurig
51a211e080 fix(sunspec): rendre la batterie SunSpec visible au moteur (energystorage)
sunspecStorage déclarait `battery` mais pas `energystorage`. Les trois
consommateurs qui filtrent par cette interface ne la trouvaient donc pas : le
bilan de puissance de nymea (`currentPowerStorage`, celui que le CLIENT lit sur
son tableau de bord), la réserve de stockage de la recharge VE, et celle du
waterfall. Vérifié sans authentification dans `energylogs.sqlite` de .75 : la
colonne `storage` de `powerBalance` est à 0.0 sur toutes les lignes, pendant
que la batterie affiche 50 % et « Holding ». Elle n'était pas à zéro, elle
était invisible.

Ce n'était pas une ligne de JSON. `energystorage` exige `currentPower` (W,
signé) et `capacity` (kWh), qu'aucun des quatorze états ne portait, et les
déclarer sans les câbler aurait donné une batterie éternellement inerte — pire
que son absence, puisque la réserve lirait « ni charge ni décharge » au lieu de
« je ne sais pas ».

D'où viennent les deux états. Le modèle 124 ne porte AUCUNE puissance : WChaMax
est un maximum, StorAval est en ampères-heures. La puissance instantanée vit
dans le modèle 160, réparti sur deux blocs `StCha` et `StDisCha` dont le `DCW`
est un uint16 toujours positif, l'inactif à zéro. D'où
`currentPower = DCW[StCha] - DCW[StDisCha]`, positif en charge, conformément à
libnymea/interfaces/energystorage.json (« positive is 'consumed' »). C'est
l'INVERSE de la convention constructeur du contrat §4, et
`FroniusRules::batteryPower()` est réécrite comme la négation stricte de la
nouvelle `storageCurrentPower()` pour qu'aucune des deux ne dérive seule.
`capacity` vient de WHRtg du modèle 120, et l'état est laissé tranquille plutôt
que mis à zéro quand le nameplate ne l'implémente pas.

Le signe est MESURÉ, pas déduit. Banc .211 unité 1, consigne batterie imposée
par MQTT, registres relus en lecture seule :
  +2000 W -> StCha 2000,0 W  StDisCha    0,0 W  ChaSt Charging
  -1500 W -> StCha    0,0 W  StDisCha 1500,0 W  ChaSt Discharging
Le même relevé a montré `DCW_SF` passant de -2 à -1 entre repos et charge : le
facteur d'échelle n'est jamais mis en cache d'un relevé à l'autre.

Écart assumé, D-9 dans DIVERGENCES.md : `sunspecStorage` est partagé avec le
vendor générique, et les interfaces se déclarent par thingClass. Un appareil
générique publiant le 124 sans le 160 porte désormais `energystorage` avec un
`currentPower` figé à zéro. Les trois alternatives sont pires — une thingClass
Fronius distincte détruirait le Thing existant et son historique. Le plugin
avertit donc UNE FOIS par thing quand il ne trouve pas de source de puissance,
en distinguant « pas de modèle 160 » de « modèle 160 sans bloc batterie ». Ça ne
remplace pas la mesure, mais ça retire le silence.

La cohérence des unités avec l'interface est vérifiée à la COMPILATION par
nymea-plugininfocompiler : mettre `KiloWattHour` sur `currentPower` fait échouer
le build. Onze tests `test_D9_*` figent le reste.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3gzsuwENJfJtvuNDT3xGn
2026-08-26 14:02:37 +02:00
Patrick Schurig
d08cf506ee fix(sunspec): lire les float32 Fronius en big endian, comme le matériel
byteOrder() renvoyait ByteOrderLittleEndian au motif qu'on « supposait le
défaut SunSpec ». La supposition confondait le défaut de la SPÉCIFICATION avec
celui de libnymea-sunspec, qui est son contraire : convertToUInt32 lit
registers.at(1) puis .at(0) en little endian. Garder ce défaut revient à lire
tous les float32 avec leurs deux mots permutés.

Mesuré le 2026-08-07 sur un Symo réel, en lecture seule : le compteur 213 sur
l'unité 200 sert Hz = 4248 0000, soit 50,0000 en big endian et 2,3777e-41 en
mots inversés. L'onduleur 113 concorde.

Le coût n'était pas un décalage mais une absurdité. Reproduit sur banc avec le
simulateur émettant l'ordre du matériel : un compteur important 2,7 kW rapporté
comme exportant 107 MW, et l'arbitre enclenchant toutes les charges sur un
« Surplus PV 107622496 W ». Le thing reste connected, sans erreur Modbus.

Conséquence sur l'ordre des correctifs : meterPowerSign() reste juste mais
n'est observable qu'une fois celui-ci appliqué — corriger le signe d'un
dénormal ne produit rien.

solarEdge force déjà big endian de son côté et le générique garde son paramètre
utilisateur : aucun des deux chemins n'est touché. test_AC_18 fige le
raisonnement plutôt que la seule valeur. Voir DIVERGENCES.md D-8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:30:14 +02:00
Patrick Schurig
ea3b9bdd51 fix(sunspec): interroger le thing de connexion, pas une recherche par id
Le garde de présence batterie demandait isFroniusChild(thing->id()), qui
résout le parent par myThings().findById(). Pendant le setup initial la
connexion n'est pas encore enregistrée : la recherche répond faux, le garde
est contourné, et l'enfant storage est créé alors qu'InBatV et StorAval sont
nuls. Constaté en boucle fermée sur le simulateur, avec la trace nymead
"Invalid parent thing id ... Not adding thing to the system".

Le garde interroge maintenant directement le thing de connexion que la
fonction tient déjà. Le signe compteur passe de même par le parentThing déjà
résolu, dont la non-nullité est garantie quelques lignes plus haut.

La suite logique ne pouvait pas attraper ce défaut : elle couvre la décision
pure, pas le câblage. Consigné en D-7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 20:05:13 +02:00
Patrick Schurig
89f7bf0199 fix(sunspec): signe du compteur Fronius et batterie fantôme
Deux défauts constatés sur le GEN24 des beaux-parents (Primo GEN24 3.0,
Smart Meter TS 65A-3, sans batterie).

Signe du compteur. onMeterBlockUpdated() négociait le signe de tous les
modèles compteur sans distinction de fabricant. Sur Fronius la négation est
fausse : le Smart Meter émet déjà dans la convention nymea. L'onduleur étant
monophasé, il sert de référence de direction : le plugin rapportait la phase
portant l'onduleur en soutirage et les deux autres en injection, ce qui est
impossible sans source. L'arbitre en concluait « Surplus insuffisant
(-842 W) » et laissait chauffe-eau et PAC éteints en plein export.

La décision vit dans FroniusRules::meterPowerSign() et n'est annulée que sous
une froniusConnection. Les vendors generic et solarEdge sont inchangés, ce
qu'un antidote verrouille. Les compteurs d'énergie sont corrects et ne sont
pas permutés.

Batterie fantôme. Un GEN24 hybride publie le modèle 124 sans batterie câblée,
d'où un enfant storage bloqué sur batteryCritical.
FroniusRules::hasBattery() exige une grandeur physique non nulle (InBatV ou
StorAval), le SoC étant écarté car 0 % ne se distingue pas d'une batterie
vide. Le côté « batterie présente » reste INFÉRÉ : voir D-6, à valider sur un
GEN24 équipé avant tout déploiement.

Suite logique : 50 tests verts, dont six antidotes AC-16 et AC-17.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 08:29:16 +02:00
Patrick Schurig
e7b751673d sunspec: record why meterSlaveId cannot be marked advanced
AC-7 asks for the meterSlaveId parameter to be advanced. That property
does not exist on nymea 1.15.2 param types: the metadata verifier
rejects unknown fields, so adding it makes the whole plugin fail to
load rather than merely hiding a parameter.

Measured by loading the plugin JSON, with and without the flag, into a
PluginMetadata linked against the installed libnymea, rather than read
from an upstream tree that may differ from the package.

Also records the three other divergences the multi unit ID work runs
into, two of which predate it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 08:50:18 +02:00