etm-powersync-plugins-modbus/BRIEF_sunspec_energystorage.md
Patrick Schurig 2f94fd741f docs(brief): SunSpec Storage doit déclarer energystorage — et pourquoi ce n'est pas une ligne
Émis par l'agent moteur. Le Thing « Fronius Storage » déclare battery + connectable,
donc trois consommateurs distincts qui le cherchent par filterByInterface(energystorage)
ne le trouvent pas : le bilan de puissance de nymea (currentPowerStorage, que le CLIENT
lit sur son tableau de bord), la réserve de stockage du proxy VE, et celle du waterfall.
Vérifié sur .75 : currentPowerStorage vaut 0 pendant que la batterie affiche 50 % et
« Holding » — elle n'est pas à zéro, elle est invisible.

Ce n'est pas une ligne de JSON : energystorage exige currentPower (signé, W) et capacity
(kWh), que sunspecStorage ne fournit pas. Les déclarer sans les câbler donnerait deux
états à zéro — et un currentPower de batterie à zéro est PIRE que son absence, puisque
la réserve lirait « ni charge ni décharge » au lieu de « je ne sais pas ».

Rappel de vigilance : d08cf50 a montré ce que coûte un float32 mal ordonné sur ce même
plugin. Un currentPower de batterie mal signé produirait la même famille de dégât, en
plus discret : la réserve croirait la batterie en charge quand elle se vide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
2026-08-26 11:57:26 +02:00

5.0 KiB

Brief — SunSpec Storage doit déclarer energystorage

Émis par : l'agent moteur (etm-powersync-energy-plugin-etm), 2026-08-26. Constaté sur : banc .75, nymea-plugin-sunspec-etm01 1.15.2+etm9. Ce n'est pas une ligne de JSON — voir §3. C'est la raison de ce brief.


1. Le constat

Thing « Fronius Storage »  →  classe « SunSpec Storage » (9a643ba8)
interfaces : ["battery", "connectable"]        ← PAS "energystorage"

Conséquence : cette batterie n'est comptée nulle part. Trois consommateurs distincts la cherchent par filterByInterface("energystorage"), et aucun ne la trouve :

Qui Ce qui est perdu
EnergyManagerImpl::updatePowerBalance() (experience-plugin nymea, l. 269) currentPowerStorage et totalFromStorage — le bilan de puissance que le client lit sur son tableau de bord
SmartChargingManager::planSurplusCharging() (l. 1170) la réserve de stockage pour la recharge du véhicule
EnergyArbitrator::buildContext() (moteur, 3g-1) la réserve de stockage pour toutes les charges pilotées

Vérifié sur .75 : Energy.GetPowerBalance rend "currentPowerStorage": 0 pendant que la batterie affiche 50 % et l'état « Holding ». Elle n'est pas à zéro : elle est invisible.

Ce qui rend ce défaut coûteux, c'est son silence. Un filtre par interface qui ne trouve rien est ambigu entre « il n'y a pas de batterie » et « il y en a une, mal déclarée ». Le second désactive une règle de sécurité sans produire ni erreur ni panne. Le moteur émet désormais un avertissement dans ce cas précis (« interface battery mais pas energystorage »), mais l'avertissement ne remplace pas la déclaration.


2. Ce que l'interface exige

libnymea/interfaces/energystorage.json :

{ "extends": ["battery", "smartmeter"],
  "states": [
    { "name": "currentPower", "type": "double", "unit": "Watt", "logged": true },
    { "name": "capacity",     "type": "double", "unit": "KiloWattHour" } ] }

3. Pourquoi ce n'est PAS une ligne de JSON

sunspecStorage déclare aujourd'hui ces états :

batteryCritical · batteryLevel · chargeGradient · chargingRate · chargingState · connected
controlHealthy · dischargeGradient · dischargingRate · enableCharging · enableDischarging
gridCharging · storageStatus · version

Ni currentPower, ni capacity. Ajouter "energystorage" aux interfaces sans ajouter les deux états produirait, au mieux, un refus de chargement du plugin ; au pire, deux états à zéro. Et un currentPower de batterie à zéro est pire que son absence : la règle de réserve lirait « la batterie ne charge ni ne décharge » au lieu de « je ne sais pas », et le bilan de puissance du client afficherait un stockage éternellement inerte.

Il faut donc câbler les deux états sur des registres :

  • currentPower — puissance instantanée du stockage, signée : positif = charge, négatif = décharge (convention nymea, cf. la description de l'interface). À établir depuis quel modèle SunSpec elle se lit sur un Symo — le modèle 124 porte l'état de charge, pas nécessairement la puissance ; elle peut devoir se déduire des canaux DC (modèle 160).
  • capacity — capacité nominale en kWh. Candidat : WHRtg du modèle 120 (plaque signalétique), à convertir en kWh.

⚠️ Attention au signe et à l'ordre des mots. Le lot d08cf50 a montré ce que coûte un float32 lu à mots permutés sur ce même plugin — un compteur important 2,7 kW rapporté comme exportant 107 MW, et des cumuls empoisonnés pendant dix-neuf jours. Un currentPower de batterie mal signé produirait la même famille de dégât, en plus discret : la réserve croirait la batterie en charge quand elle se vide.


4. Ce qui est demandé

  1. Garder battery — ne pas la remplacer. D'autres choses peuvent en dépendre, et energystorage l'étend de toute façon.
  2. Ajouter les états currentPower et capacity, câblés sur les bons registres et avec le bon signe.
  3. Puis ajouter "energystorage" aux interfaces de sunspecStorage.
  4. Vérifier sur .75 que Energy.GetPowerBalance rend enfin un currentPowerStorage non nul et cohérent avec l'état de la batterie.

Une fois cela fait, le moteur pourra éprouver sa règle de réserve pour la première fois — la bascule batteryLevelConsideration 0,4 / 0,9 sur un banc dont la batterie est à 50 %.


5. Question ouverte, à trancher côté SunSpec

Les autres classes du plugin sont-elles dans le même cas ? Sur .75, l'onduleur déclare bien smartmeterproducer + smartmeter et le compteur smartmeter — donc eux sont vus. Mais le relevé du lot D avait noté que l'onduleur ne publie aucun cumul (thingCache à 0.0 / 0.0), ce qui gèle totalProduction côté nymea. Cause différente du présent brief — il est trouvé, mais il ne publie pas totalEnergyProduced — et qui mérite le même examen.