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
357 lines
18 KiB
Markdown
357 lines
18 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.
|
||
|
||
---
|
||
|
||
## D-8 — ordre des mots des float32 : le défaut de la bibliothèque n'est pas celui de la spec (MESURÉ)
|
||
|
||
**Contexte.** `FroniusRules::byteOrder()` renvoyait `ByteOrderLittleEndian`, avec
|
||
pour justification « non documenté par le constructeur, on suppose le défaut
|
||
SunSpec ».
|
||
|
||
**L'écart.** La supposition confondait deux « défauts ». Un float32 SunSpec
|
||
occupe deux registres, **mot de poids fort d'abord**. Or
|
||
`SunSpecDataPoint::convertToUInt32` lit `registers.at(0)` puis `.at(1)` en
|
||
`ByteOrderBigEndian`, et l'**inverse** en `ByteOrderLittleEndian` — qui est le
|
||
défaut de libnymea-sunspec. Garder ce défaut revient à lire tous les float32 de
|
||
la personnalité Fronius avec leurs deux mots permutés.
|
||
|
||
**Mesure.** Relevé du 2026-08-07 sur un Fronius Symo réel, en lecture seule
|
||
(`sunspec/tools/read-float-order.py`) :
|
||
|
||
| point | registres bruts | big endian | mots inversés |
|
||
|---|---|---|---|
|
||
| compteur 213 u200 `Hz` | `4248 0000` | **50,0000** | 2,3777e−41 |
|
||
| compteur 213 u200 `PhVphA` | `436a 8000` | **234,5000** | −2,4184e−41 |
|
||
| onduleur 113 u1 `Hz` | `4247 fc48` | **49,9964** | −4,1592e+36 |
|
||
|
||
**Ce que cela coûtait.** Pas un décalage : une absurdité. Reproduit sur banc avec
|
||
le simulateur émettant l'ordre du matériel — un compteur **important 2,7 kW** a
|
||
été rapporté comme **exportant 107 MW**, `Current load on root meter:
|
||
-1.07621e+08W`, et l'arbitre a enclenché toutes les charges sur un « Surplus PV
|
||
107622496 W ». Un tel appareil se présente `connected = true`, sans aucune
|
||
erreur Modbus.
|
||
|
||
**Ordre des correctifs.** `meterPowerSign()` (D-5) reste juste, mais n'est
|
||
**observable** qu'une fois l'ordre des mots corrigé : corriger le signe d'un
|
||
dénormal ne produit rien.
|
||
|
||
**Non touché.** Le vendor solarEdge force déjà big endian sur son propre chemin ;
|
||
le vendor générique conserve son paramètre `endianness` réglable par
|
||
l'utilisateur. Aucun des deux n'est modifié.
|
||
|
||
**Verrou.** `test_AC_6` affirme désormais `ByteOrderBigEndian`, et
|
||
`test_AC_18_swapped_words_yield_denormals` fige le raisonnement : les registres
|
||
réels lus à l'envers donnent un dénormal, jamais une mesure plausible.
|
||
|
||
---
|
||
|
||
## D-9 — `sunspecStorage` déclare `energystorage` mais tous ses porteurs ne savent pas mesurer (MESURÉ)
|
||
|
||
**Ce que demande le brief.** `BRIEF_sunspec_energystorage.md` : le Thing déclarait
|
||
`battery` + `connectable`, donc les trois consommateurs qui filtrent par
|
||
`filterByInterface("energystorage")` ne le trouvaient pas — le bilan de puissance
|
||
que le client lit, la réserve de recharge VE, la réserve du waterfall. Sur `.75`,
|
||
`currentPowerStorage` valait 0 pendant que la batterie affichait 50 % et
|
||
« Holding » : elle n'était pas à zéro, elle était invisible.
|
||
|
||
**Ce que fait le code.** `sunspecStorage` déclare désormais
|
||
`["battery", "energystorage", "connectable"]` et porte `currentPower` (W) et
|
||
`capacity` (kWh), câblés sur des registres.
|
||
|
||
### D'où vient `currentPower`
|
||
|
||
**Le modèle 124 ne porte aucune puissance.** Il décrit ce que la batterie *est*
|
||
— SoC, statut, consignes — pas ce qu'elle *fait* en watts. `WChaMax` est une
|
||
puissance **maximale**, `StorAval` est en ampères-heures. Vérifié registre par
|
||
registre sur le banc.
|
||
|
||
La puissance instantanée vit dans le **modèle 160**, réparti sur deux blocs
|
||
distincts que Fronius nomme `StCha` (charge) et `StDisCha` (décharge). `DCW` est
|
||
un `uint16` : chaque bloc est toujours positif, et le bloc inactif est à zéro.
|
||
D'où
|
||
|
||
```
|
||
currentPower = DCW[StCha] − DCW[StDisCha]
|
||
```
|
||
|
||
**Le signe est mesuré, pas déduit.** Relevé du 2026-08-26, banc `.211` unité 1,
|
||
modèle 160 à 40265, consigne batterie imposée par MQTT (`banc/cmd/manual`) et
|
||
registres relus en lecture seule :
|
||
|
||
| consigne banc | `banc/etat/batterie` | `StCha` | `StDisCha` | `ChaSt` (124) | `StCha − StDisCha` |
|
||
|---|---|---|---|---|---|
|
||
| +2000 W (charge) | +2000,0 W | **2000,0 W** | 0,0 W | Charging | **+2000,0 W** |
|
||
| −1500 W (décharge) | −1500,0 W | 0,0 W | **1500,0 W** | Discharging | **−1500,0 W** |
|
||
| 0 W | 0,0 W | 0,0 W | 0,0 W | Holding | 0,0 W |
|
||
|
||
`libnymea/interfaces/energystorage.json` : « positive is 'consumed' ». Une
|
||
batterie qui charge consomme, donc charge = positif. C'est **l'inverse** de la
|
||
convention constructeur du contrat §4 et de `FroniusRules::batteryPower()`. Les
|
||
deux fonctions coexistent, et `batteryPower()` est écrite comme la négation de
|
||
`storageCurrentPower()` pour qu'aucune des deux ne puisse dériver seule.
|
||
|
||
**Ce que le relevé a montré en plus.** `DCW_SF` est passé de **−2 à −1** entre la
|
||
lecture au repos et la lecture en charge, sans hystérésis (I5 du contrat). Un
|
||
plugin qui mettrait ce facteur en cache à la découverte lirait des puissances
|
||
fausses d'un facteur 10. libnymea-sunspec ne le met pas en cache : les 90
|
||
registres du modèle 160 sont lus en une seule transaction Modbus, et
|
||
`processBlockData()` relit les facteurs d'échelle du bloc fixe **avant** de
|
||
traiter les blocs répétés. La propriété est donc tenue, mais elle tient à cet
|
||
ordre — la casser en amont casserait cette lecture sans aucun signe extérieur.
|
||
|
||
### D'où vient `capacity`
|
||
|
||
`WHRtg` du modèle 120 (plaque signalétique), Wh → kWh.
|
||
`SunSpecStorageLogic::nameplateCapacityKwh()` rend une valeur **négative** quand
|
||
le nameplate n'en porte pas, et le plugin laisse alors l'état tranquille plutôt
|
||
que de publier un zéro qui se lirait comme une mesure.
|
||
|
||
**Non vérifiable au banc.** Le simulateur sert les modèles 120 à 123 en
|
||
**coquilles vides** (choix assumé du lot 1) : les 26 registres du 120 sont à
|
||
zéro, `WHRtg` compris. `capacity` reste donc à 0 sur `.75`, et le chemin de
|
||
lecture n'est éprouvé que par les tests unitaires. **Comment lever** : peupler
|
||
`WHRtg` dans la vue `sunspec-fronius` du banc, ou relever le modèle 120 d'un
|
||
GEN24 réel équipé d'une batterie. Aucun des trois consommateurs cités par le
|
||
brief ne lit `capacity` — la conséquence d'un 0 est bornée à l'affichage.
|
||
|
||
### L'écart assumé
|
||
|
||
**`sunspecStorage` est partagé entre les vendors `sunspec` générique et Fronius**,
|
||
et les interfaces se déclarent par thingClass, pas par thing. Un appareil SunSpec
|
||
générique qui publie le modèle 124 **sans** modèle 160 — c'est le cas du
|
||
simulateur générique `.210` — porte désormais l'interface `energystorage` avec un
|
||
`currentPower` qui ne bougera jamais de zéro. C'est exactement le défaut que le
|
||
brief décrit : un zéro se lit « ni charge ni décharge » là où l'absence se lisait
|
||
« je ne sais pas ».
|
||
|
||
**Pourquoi l'accepter quand même.** Les trois alternatives sont pires :
|
||
|
||
- créer une thingClass Fronius distincte (à la `solarEdgeBattery`) contredit le
|
||
contrat §A′ ligne 44, et **détruit le Thing existant** de `.75` avec son
|
||
historique, puisque le balayage des orphelins ferait disparaître l'ancien ;
|
||
- ne pas déclarer l'interface laisse les trois consommateurs aveugles, ce qui est
|
||
le défaut d'origine ;
|
||
- refuser de créer l'enfant storage quand la puissance n'est pas mesurable ferait
|
||
disparaître des appareils génériques qui fonctionnent aujourd'hui.
|
||
|
||
**Ce qui rend l'écart audible.** Le plugin **avertit une fois par thing** quand il
|
||
ne trouve pas de source de puissance, en distinguant les deux cas :
|
||
pas de modèle 160 du tout sur la connexion, ou modèle 160 sans bloc
|
||
`StCha`/`StDisCha` (un GEN24 sans batterie publie `N=2`). L'avertissement ne
|
||
remplace pas la mesure, mais il retire le silence — qui était le vrai coût du
|
||
défaut d'origine.
|
||
|
||
**Comment lever.** Que nymea permette de déclarer une interface par thing et non
|
||
par thingClass, ou que le vendor générique reçoive sa propre thingClass storage
|
||
au moment où un appareil générique avec batterie entre réellement au parc.
|
||
|
||
**Verrous.** `test_D9_*` dans `tests/logic` : la déclaration d'interface, les deux
|
||
états et leurs unités, le signe dans les deux sens, l'opposition stricte avec
|
||
`batteryPower()`, l'exclusion des strings PV, et la distinction entre « batterie
|
||
à l'arrêt » et « pas de bloc batterie ». La cohérence des unités avec l'interface
|
||
est en outre vérifiée **à la compilation** par `nymea-plugininfocompiler` :
|
||
essayer `KiloWattHour` sur `currentPower` fait échouer le build avec
|
||
« claims to implement interface "energystorage" but state "currentPower" has not
|
||
matching unit ».
|