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>
This commit is contained in:
Patrick Schurig 2026-08-05 20:05:13 +02:00
parent c459ba2f9b
commit ea3b9bdd51
2 changed files with 34 additions and 2 deletions

View File

@ -173,3 +173,30 @@ installée porte une tension au repos » est donc **INFÉRÉE des deux côtés**
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.

View File

@ -841,8 +841,12 @@ void IntegrationPluginSunSpec::processDiscoveryResult(Thing *thing, SunSpecConne
* phantom battery stuck on batteryCritical. Only the Fronius
* path is gated; the generic and solarEdge vendors keep their
* previous behaviour. */
/* Asked of the connection thing we already hold, not through
* isFroniusChild(): during the initial setup the parent is not
* registered in myThings() yet, so a lookup answers false and
* the gate would be skipped exactly when it is needed. */
SunSpecStorageModel *storageModel = qobject_cast<SunSpecStorageModel *>(model);
if (storageModel && isFroniusChild(thing->id())
if (storageModel && thing->thingClassId() == froniusConnectionThingClassId
&& !FroniusRules::hasBattery(storageModel->inBatV(), storageModel->storAval())) {
qCDebug(dcSunSpec()) << "Fronius model 124 found but no battery is installed"
<< "(InBatV" << storageModel->inBatV()
@ -1853,7 +1857,8 @@ void IntegrationPluginSunSpec::onMeterBlockUpdated()
/* Sign applied to every power reading of this block. The generic vendors
* keep the historical negation; a meter sitting under a Fronius connection
* already reports in the nymea convention. See FroniusRules. */
const double powerSign = FroniusRules::meterPowerSign(isFroniusChild(thing->parentId()));
const double powerSign = FroniusRules::meterPowerSign(
parentThing->thingClassId() == froniusConnectionThingClassId);
switch (model->modelId()) {
case SunSpecModelFactory::ModelIdMeterSinglePhase: {