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>
203 lines
10 KiB
Markdown
203 lines
10 KiB
Markdown
# Divergences — vendor Fronius du plugin SunSpec
|
||
|
||
Écarts assumés entre ce que demandent `specs/fronius-sunspec.md` et le contrat
|
||
d'une part, et ce que le code fait d'autre part. Chaque entrée dit **pourquoi**
|
||
et **comment la lever**.
|
||
|
||
Deux étiquettes :
|
||
|
||
- **MESURÉ** — l'écart repose sur une observation faite sur du matériel ou sur
|
||
le code de la plateforme, reproductible.
|
||
- **INFÉRÉ** — l'écart repose sur un raisonnement, faute de donnée. À reprendre
|
||
dès qu'une mesure existe.
|
||
|
||
---
|
||
|
||
## D-1 — `meterSlaveId` n'est pas marqué `advanced` (MESURÉ)
|
||
|
||
**Ce que demande AC-7.** Le paramètre `meterSlaveId` de la thingClass
|
||
`froniusConnection` doit être `advanced`.
|
||
|
||
**Ce que fait le code.** Rien : le paramètre n'a pas ce drapeau.
|
||
|
||
**Pourquoi.** `advanced` n'est pas une propriété de paramType dans nymea 1.15.2.
|
||
`ParamType::typeProperties()` énumère exactement `id`, `name`, `displayName`,
|
||
`type`, `defaultValue`, `inputType`, `unit`, `minValue`, `maxValue`, `stepSize`,
|
||
`allowedValues`, `readOnly`. Le vérificateur de métadonnée refuse tout champ
|
||
inconnu :
|
||
|
||
```
|
||
Param type "meterSlaveId" has unknown fields: "advanced"
|
||
```
|
||
|
||
et `PluginMetadata::isValid()` passe à `false`. Ce n'est pas un paramètre mal
|
||
affiché : **le plugin ne se charge plus du tout**, et les erreurs suivantes en
|
||
cascade (le paramType est abandonné, donc `froniusConnection` ne satisfait plus
|
||
l'interface `networkdevice`) rendent le diagnostic trompeur.
|
||
|
||
Vérifié en chargeant le JSON, avec et sans le drapeau, dans un
|
||
`PluginMetadata` lié à la `libnymea` installée — pas seulement lu dans une
|
||
source amont qui pourrait diverger du paquet.
|
||
|
||
**Comment lever.** Rien à faire côté plugin. Il faut soit que nymea ajoute la
|
||
propriété, soit que AC-7 soit corrigé. Un test de garde
|
||
(`test_F3_meter_slave_id_is_not_marked_advanced`) empêche qu'on ajoute le
|
||
drapeau sans refaire la mesure.
|
||
|
||
---
|
||
|
||
## D-2 — délai de garde du balayage des enfants orphelins (INFÉRÉ)
|
||
|
||
**Contexte.** `processDiscoveryResult()` fait disparaître les things enfants
|
||
dont le modèle n'est plus annoncé. Avec deux connexions sous le même parent,
|
||
chacune ne voit que la moitié du tableau : balayer sur cette moitié ferait
|
||
disparaître les enfants de l'autre à chaque découverte. Le balayage attend donc
|
||
que **toutes** les connexions du parent aient réussi une découverte, puis
|
||
confronte les enfants à l'union de leurs modèles.
|
||
|
||
**L'écart.** Une connexion dont la découverte n'aboutit jamais bloquerait le
|
||
balayage pour toujours. Un délai de garde le débloque :
|
||
`OrphanSweepGraceSeconds = 600` (10 minutes).
|
||
|
||
**Pourquoi cette valeur, et pourquoi elle est INFÉRÉE.** Aucune donnée ne la
|
||
fixe. Le choix est asymétrique et volontairement paresseux : un balayage trop
|
||
tardif laisse traîner un thing périmé affiché déconnecté, un balayage trop
|
||
précoce **supprime un thing de l'utilisateur**. À conséquences inégales,
|
||
réglage prudent. 10 minutes est très au-delà de tout cycle de découverte
|
||
observé, sans être infini.
|
||
|
||
**Comment lever.** Mesurer, sur le banc puis sur matériel, le temps que met une
|
||
découverte SunSpec à échouer définitivement sur un unit ID qui ne répond pas
|
||
(timeout × retries × nombre de registres de base balayés). Le délai de garde
|
||
doit valoir quelques fois cette durée, pas dix minutes arbitraires. La valeur
|
||
est un seul `const` dans `integrationpluginsunspec.cpp` ; la changer n'a
|
||
d'effet sur rien d'autre.
|
||
|
||
---
|
||
|
||
## D-3 — la configuration du plugin écrase les défauts de cadence Fronius (MESURÉ, préexistant)
|
||
|
||
**Contexte.** `createConnection()` impose `timeout` et `numberOfRetries` du
|
||
vendor Fronius (AC-8) à la création. Mais `onPluginConfigurationChanged()`
|
||
réapplique ensuite les valeurs globales du plugin à **toutes** les connexions,
|
||
Fronius comprise.
|
||
|
||
**L'écart.** Il est antérieur à F3 et affecte déjà la connexion onduleur seule.
|
||
F3 ne le corrige pas ; il l'étend symétriquement à la connexion compteur, pour
|
||
que les deux connexions d'un même thing se comportent identiquement. Le
|
||
plancher d'intervalle d'interrogation d'AC-8, lui, reste appliqué : il vit dans
|
||
`effectiveUpdateInterval()`, pas ici.
|
||
|
||
**Comment lever.** Hors périmètre de F3 : cela demande de décider si les
|
||
réglages globaux du plugin doivent pouvoir passer outre un défaut vendor, ce que
|
||
ni le contrat ni la spec ne tranchent.
|
||
|
||
---
|
||
|
||
## D-4 — connexion détruite par `ThingSetupInfo::aborted` et laissée dans la table (MESURÉ, préexistant, amont)
|
||
|
||
**Contexte.** `setupThing()` relie `ThingSetupInfo::aborted` à
|
||
`SunSpecConnection::deleteLater()`, alors que `createConnection()` a déjà inséré
|
||
la connexion dans `m_sunSpecConnections`. Un setup avorté laisse donc un
|
||
pointeur pendant dans la table ; le setup suivant du même thing prend la branche
|
||
« reconfiguration » et appelle `deleteLater()` sur un objet déjà détruit.
|
||
|
||
**L'écart.** F3 ne corrige pas ce chemin pour la connexion primaire : il est
|
||
commun aux trois vendors, y compris `solarEdge` dont aucun test ne couvre le
|
||
comportement, et le périmètre du lot est explicitement « generic et
|
||
solarEdgeConnection ne changent pas de comportement ». La connexion **compteur**
|
||
ajoutée par F3, elle, se retire proprement de sa table à la destruction.
|
||
|
||
**Comment lever.** Relier `QObject::destroyed` au retrait de la table pour les
|
||
deux connexions, dans un lot qui assume de toucher le chemin commun, avec une
|
||
couverture de test pour `solarEdge`.
|
||
|
||
---
|
||
|
||
## D-5 — le signe du compteur est corrigé côté Fronius seulement (MESURÉ)
|
||
|
||
**Contexte.** `onMeterBlockUpdated()` négociait le signe de façon **générique** :
|
||
tous les modèles compteur (201, 202, 203, 204, 211…214) faisaient
|
||
`-meter->watts()`. Il n'existait aucun mapping spécifique à un fabricant.
|
||
|
||
**L'écart.** Sur Fronius cette négation est fausse : le Smart Meter émet déjà
|
||
dans la convention nymea (négatif = injection). Le correctif n'annule la
|
||
négation que lorsque le compteur est enfant d'une `froniusConnection` ; les
|
||
vendors generic et solarEdge gardent la négation historique, verrouillée par
|
||
`test_AC_16_generic_meter_keeps_the_historical_negation`.
|
||
|
||
**Preuve.** Relevé du 02/08/2026 sur un Primo GEN24 3.0, onduleur **monophasé**
|
||
(modèle 101) et Smart Meter TS 65A-3 (modèle **203**, pas 213). L'onduleur
|
||
n'injecte que sur la phase A ; le plugin rapportait phase A = +1003 W
|
||
(soutirage) et phases B et C à −33 W et −262 W, soit une injection sur deux
|
||
phases ne portant aucune source. En inversant, le bilan se referme : production
|
||
1190 W = charge phase A 187 W + export 1003 W ; import B+C 294 W ; consommation
|
||
481 W ; export réseau 709 W contre 708 W mesuré. Corroboration indépendante :
|
||
sur la même fenêtre, le compteur d'énergie *import* de l'appareil reste à 0
|
||
pendant que l'*export* s'incrémente. Conséquence observée :
|
||
`[Arbitre] "chauffe-eau" → "Surplus insuffisant (-842 W)"` alors que le site
|
||
exportait ~842 W.
|
||
|
||
**Non touché.** Les compteurs d'énergie (`totalEnergyConsumed` /
|
||
`totalEnergyProduced`) sont **corrects** et ne sont pas permutés : ce sont eux
|
||
qui servent de témoin à charge ci-dessus.
|
||
|
||
**Comment lever.** Déterminer si la négation générique est fausse pour tous les
|
||
fabricants ou seulement pour Fronius demanderait un compteur d'un autre vendor
|
||
sur banc. Tant que ce n'est pas tranché, le générique ne bouge pas.
|
||
|
||
---
|
||
|
||
## D-6 — présence batterie : le côté « batterie présente » n'est pas validé (INFÉRÉ)
|
||
|
||
**Contexte.** Un GEN24 hybride publie le modèle 124 même sans batterie câblée.
|
||
Le plugin créait donc un enfant storage systématiquement, d'où batterie fantôme
|
||
et `batteryCritical` permanent.
|
||
|
||
**L'écart.** `FroniusRules::hasBattery()` exige désormais `InBatV > 0` ou
|
||
`StorAval > 0` pour créer l'enfant, et seulement sous une `froniusConnection`.
|
||
Le SoC est écarté (0 % ne se distingue pas d'une batterie vide) ainsi que les
|
||
registres de consigne (`WChaMax`… gardent leurs défauts sans batterie).
|
||
|
||
**Ce qui est établi.** Le côté « absente » est confirmé sur matériel : le GEN24
|
||
des beaux-parents, sans batterie, exposait le modèle 124 avec `ChaSt = Off` et
|
||
un SoC de 0, et nymea journalisait « There is no energy storage available to
|
||
consider » pendant que l'enfant restait visible.
|
||
|
||
**Ce qui ne l'est pas.** `InBatV` et `StorAval` n'étant pas exposés en states
|
||
nymea, leurs valeurs sur ce GEN24 sans batterie **n'ont pas pu être relevées**,
|
||
et aucun GEN24 **avec** batterie n'était joignable. L'hypothèse « une batterie
|
||
installée porte une tension au repos » est donc **INFÉRÉE des deux côtés**.
|
||
|
||
**Comment lever.** Relever en lecture seule le bloc 124 d'un GEN24 équipé d'une
|
||
batterie et vérifier `InBatV`/`StorAval` non nuls. **À faire avant tout
|
||
déploiement** : un discriminateur trop strict ferait disparaître une vraie
|
||
batterie chez un client.
|
||
|
||
---
|
||
|
||
## D-7 — la présence batterie ne peut pas être vérifiée par la suite logique (MESURÉ)
|
||
|
||
**Contexte.** La première version du garde de D-6 demandait
|
||
`isFroniusChild(thing->id())`, qui résout le parent par `myThings().findById()`.
|
||
|
||
**L'écart.** Pendant le setup initial, la connexion parente n'est **pas encore
|
||
enregistrée** dans `myThings()` : la recherche répond faux, le garde Fronius est
|
||
contourné, et l'enfant storage est créé malgré `InBatV = StorAval = 0`. Trace
|
||
nymead correspondante :
|
||
`ThingManager: Invalid parent thing id … Not adding thing to the system.`
|
||
|
||
Le garde interroge désormais directement le `thing` de connexion que la fonction
|
||
tient déjà (`thing->thingClassId() == froniusConnectionThingClassId`), sans
|
||
aucune recherche. Même traitement pour le signe compteur, qui utilise le
|
||
`parentThing` déjà résolu et non nul.
|
||
|
||
**Ce que cela apprend.** La suite `tests/logic` ne peut pas attraper ce défaut :
|
||
elle couvre la décision pure (`FroniusRules`), pas le câblage du plugin, c'est à
|
||
dire *quel prédicat est interrogé et à quel moment du cycle de vie*. Seul un
|
||
essai en boucle fermée contre un simulateur le révèle. Toute modification du
|
||
garde de présence doit donc être rejouée sur banc, pas seulement en unitaire.
|
||
|
||
**Comment lever.** Étendre `tests/plugin`, qui instancie réellement le plugin, à
|
||
un scénario de setup initial avec un modèle 124 à zéro. Hors périmètre ici.
|