From 122df17a98a2f18dc78bf56a4d8f40035c12dbe7 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Wed, 26 Aug 2026 15:45:40 +0200 Subject: [PATCH] =?UTF-8?q?docs(sunspec):=20=C3=A9crire=20la=20d=C3=A9pend?= =?UTF-8?q?ance=20=C3=A0=20l'ordre=20de=20lecture=20des=20SF=20l=C3=A0=20o?= =?UTF-8?q?=C3=B9=20elle=20porte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le relevé du 2026-08-26 a montré DCW_SF passant de -2 à -1 entre une batterie à l'arrêt et une batterie en charge, sur le même onduleur, à quelques secondes d'intervalle. Un DCW brut de 20000 vaut 200,0 W avec le premier facteur et 2000,0 W avec le second. Ce que ça casserait est de la même famille que d08cf50 mais plus discret : pas de dénormal, pas d'erreur Modbus, un appareil qui se déclare connected, et une puissance simplement dix fois fausse. La propriété tient aujourd'hui parce que libnymea-sunspec lit les 90 registres du modèle 160 en une transaction et relit les facteurs du bloc fixe AVANT les blocs répétés — deux propriétés qui vivent en amont, que rien dans le compilateur ne tient, et qu'une optimisation de la lecture ferait sauter sans aucun signe extérieur. Le commentaire est donc à l'endroit qui en dépend : au-dessus de storageModuleReadings(), qui consomme dcPower(), avec un pointeur au site de readBlockData() et sur le champ ModuleReading::dcPower. Le README documente en outre le côté exploitation de D-9 : un onduleur qui publie le 124 sans le 160 porte energystorage avec un currentPower figé à zéro, avec les deux lignes de journal exactes à chercher et la commande pour les trouver. --- sunspec/README.md | 57 +++++++++++++++++++++++++++- sunspec/froniusrules.h | 7 ++++ sunspec/integrationpluginsunspec.cpp | 41 +++++++++++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/sunspec/README.md b/sunspec/README.md index 682dd1a..b6565cc 100644 --- a/sunspec/README.md +++ b/sunspec/README.md @@ -13,7 +13,9 @@ Connect nymea to SunSpec devices over Modbus TCP * Model ID 202 & 212 Split phase meter * Model ID 203 & 213 Three phase meter * SunSpec Storage - * Model ID 124 + * Model ID 124 (state of charge, status, setpoints) + * Model ID 160 (instantaneous power, `StCha` / `StDisCha` blocks) + * Model ID 120 (rated capacity, `WHRtg`) ## Tested connections @@ -27,3 +29,56 @@ Connect nymea to SunSpec devices over Modbus TCP ## More https://sunspec.org + +--- + +## Exploitation — batterie visible mais non mesurable + +Depuis `1.15.2+etm10`, la thingClass `SunSpec Storage` déclare l'interface +**`energystorage`**. C'est ce qui la rend visible au bilan de puissance de nymea +(`currentPowerStorage`), à la réserve de recharge du véhicule, et à la réserve du +waterfall. Avant, elle ne déclarait que `battery` et **aucun des trois ne la +trouvait**. + +L'état `currentPower` se lit dans le **modèle 160**, pas dans le modèle 124 qui +n'a aucun registre de puissance. Conséquence à connaître avant de diagnostiquer +une batterie « à zéro » : + +> **Un onduleur SunSpec qui publie le modèle 124 sans modèle 160 porte +> l'interface `energystorage` avec un `currentPower` qui ne bougera jamais de +> zéro.** C'est le cas des appareils SunSpec génériques. La batterie apparaît +> alors dans le bilan, comptée pour 0 W — ce qui se lit « ni charge ni +> décharge », et non « je ne sais pas ». + +Ce cas est **annoncé une fois par thing** dans le journal de `nymead`, au +démarrage ou au premier relevé qui suit la découverte. Deux formulations, selon +la cause : + +``` +W | SunSpec: Storage "" has no model 160 on its connection: currentPower + has no register to read and will stay at 0. This storage is visible to the + energy balance but not measurable. + +W | SunSpec: Storage "" has a model 160 but no StCha/StDisCha block: + currentPower has nothing to read and will stay at 0. +``` + +La seconde forme est le cas d'un onduleur hybride **sans batterie câblée** : il +publie `N = 2` (les deux strings PV) et aucun bloc batterie. + +À vérifier en premier sur un site où `currentPowerStorage` reste à zéro : + +```bash +journalctl -u nymead | grep -i "will stay at 0" +``` + +Si la ligne est là, le défaut est dans ce que l'appareil publie, pas dans nymea. +Si elle est absente, la batterie est mesurée et le zéro est une vraie mesure. + +L'état `capacity` (kWh) vient de `WHRtg` du modèle 120. Quand la plaque +signalétique ne l'implémente pas, l'état est **laissé tel quel** plutôt que remis +à zéro : un zéro se lirait comme une mesure. Aucune règle du moteur ne lit +`capacity` à ce jour. + +Détail du raisonnement, de la mesure du signe et de l'écart assumé : +`DIVERGENCES.md`, entrée **D-9**. diff --git a/sunspec/froniusrules.h b/sunspec/froniusrules.h index 3a72b14..2c6fcdc 100644 --- a/sunspec/froniusrules.h +++ b/sunspec/froniusrules.h @@ -198,6 +198,13 @@ BlockRole classifyBlock(const QString &inputIdString); struct ModuleReading { QString inputIdString; + + /* Already scaled by DCW_SF, in watts. That factor is auto-scaled by the + * inverter at runtime - measured moving from -2 to -1 between an idle and a + * charging battery on the same machine - so it is only valid alongside the + * powers of the same Modbus transaction. Whoever fills this struct owns + * that guarantee; see the read order note above + * IntegrationPluginSunSpec::storageModuleReadings(). */ double dcPower; }; diff --git a/sunspec/integrationpluginsunspec.cpp b/sunspec/integrationpluginsunspec.cpp index ac6f737..87252a9 100644 --- a/sunspec/integrationpluginsunspec.cpp +++ b/sunspec/integrationpluginsunspec.cpp @@ -1662,7 +1662,12 @@ void IntegrationPluginSunSpec::onRefreshTimer() /* Companion models of the storage things. Deduplicated: two storage things * on the same inverter share one model 160, and reading it twice per cycle - * would double the Modbus traffic for the same 90 registers. */ + * would double the Modbus traffic for the same 90 registers. + * + * Whole model, one readBlockData() each, never a partial read: the power of + * the battery blocks is only meaningful next to the scale factor of the + * same transaction. See the read order note above + * storageModuleReadings(). */ QSet storageCompanions; foreach (SunSpecMpptModel *model, m_storageMpptModels.values()) storageCompanions.insert(model); @@ -2193,6 +2198,40 @@ void IntegrationPluginSunSpec::bindStorageCompanionModels(Thing *thing, SunSpecM updateStorageCapacity(thing); } +/* ORDRE DE LECTURE - ne pas "optimiser" la lecture du modele 160. + * + * `dcPower()` rend une valeur DEJA multipliee par `DCW_SF`. Ce facteur d'echelle + * n'est pas une constante de l'appareil : Fronius l'auto-scale EN + * FONCTIONNEMENT, sans hysteresis. Mesure du 2026-08-26 sur le banc, meme + * onduleur, deux relevees a quelques secondes d'intervalle : + * + * batterie a l'arret, PV faible -> DCW_SF = -2 + * batterie en charge 2000 W -> DCW_SF = -1 + * + * Un `DCW` brut de 20000 vaut donc 200,0 W avec le premier facteur et 2000,0 W + * avec le second. Lire la puissance avec un facteur qui n'est pas celui de la + * MEME transaction, c'est se tromper d'un ordre de grandeur - meme famille de + * degat que l'ordre des mots des float32 (D-8), mais bien plus discret : la + * valeur reste plausible, elle est juste dix fois trop petite ou trop grande. + * + * Ce qui protege aujourd'hui : + * + * 1. `SunSpecModel::readBlockData()` lit les 90 registres du modele 160 en UNE + * transaction Modbus (la limite de decoupage est a 125 registres), donc les + * facteurs d'echelle et les puissances viennent du meme instantane ; + * 2. `SunSpecModel::handleNewBlockData()` appelle `processBlockData()` du bloc + * FIXE - qui relit `DCA_SF`/`DCV_SF`/`DCW_SF`/`DCWH_SF` - AVANT de traiter + * les blocs repetes, qui lisent ce facteur chez leur parent. + * + * Ces deux proprietes sont dans libnymea-sunspec, pas ici, et rien dans le + * compilateur ne les tient. Les casser en amont - decouper la lecture, mettre + * les facteurs en cache a la decouverte, traiter les blocs repetes en premier, + * ou lire un bloc repete isolement - casserait cette fonction SANS AUCUN SIGNE + * EXTERIEUR : pas d'erreur Modbus, pas de denormal, un appareil qui se declare + * `connected`. Toute reprise de la lecture du modele 160 doit etre rejouee au + * banc contre une puissance batterie connue, pas seulement compilee. + * + * Contrat Fronius I5, et D-9 dans DIVERGENCES.md. */ QList IntegrationPluginSunSpec::storageModuleReadings(Thing *thing) const { QList readings;