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;