etm-powersync-plugins-modbus/specs/fronius-sunspec.md
Patrick Schurig 93ecfb4ca1 specs: add acceptance criteria for Fronius SunSpec support
AC-1..AC-4 are the four upstream anomalies of the generic sunspec vendor
(setpoint range, read/write mismatch on the rate states, enableDischarging
never reconciled, revert timer never written).

AC-5..AC-15 are the Fronius-specific behaviour: manufacturer filter, byte
order, connection parameters, polling floor, forbidden writes, the HEMS
intent to register table, scale factor reread, readback verification and
the model 160 block classification.

Points the design contract leaves open are recorded as explicit working
hypotheses (H-1..H-6) rather than blocking on them.
2026-07-31 10:16:06 +02:00

17 KiB
Raw Blame History

Spec — support Fronius du plugin SunSpec

Critères d'acceptation numérotés, dérivés du contrat de conception Fronius (document interne, non versionné ici) et du brief de lot 0.

Deux natures de critères, à ne jamais mélanger dans un même commit :

Bloc Nature Commits
AC-1 … AC-4 correctifs d'anomalies présentes dans nymea 1.15.2 amont message en anglais, aucune référence ETM / Fronius / PowerSync — ils ont vocation à être proposés en amont
AC-5 … AC-15 spécifique ETM (vendor fronius) commits séparés, préfixe libre

Statut des critères : voir ## Tableau de statut en fin de document.


Invariants applicables à toute l'implémentation

Ces règles ne sont pas des critères testables un à un ; elles conditionnent chaque AC ci-dessous.

  • INV-1 — aucune adresse de registre en dur. Toute lecture et toute écriture passent par la découverte dynamique puis par les offsets du modèle. Le nombre de blocs répétitifs du modèle 160 décale l'adresse absolue du modèle 124, et sans batterie le modèle 124 est absent : une adresse absolue n'est jamais valide durablement. Les adresses des documentations constructeur servent à valider un dump, jamais à adresser.
  • INV-2 — libnymea-sunspec n'est jamais modifiée. Tout vit dans sunspec/. Voir sunspec/VENDORED.md.
  • INV-3 — une consigne écrite est relue. Plusieurs onduleurs acceptent une écriture puis l'ignorent sans code d'exception Modbus.
  • INV-4 — la régulation se ferme sur la puissance mesurée, jamais sur la consigne émise.
  • INV-5 — les scale factors ne sont pas statiques. Ils changent avec le firmware et en fonctionnement (auto-scale). Relire le SF concerné immédiatement avant conversion et avant écriture.

Bloc 0 — correctifs upstream

AC-1 — plage des consignes de charge/décharge (anomalie A1)

chargingRate et dischargingRate sont déclarés minValue: 0, maxValue: 100 dans integrationpluginsunspec.json. La plage réelle des points InWRte / OutWRte du modèle 124 est −100,00 … +100,00 (int16, SF −2).

Critère. Dans integrationpluginsunspec.json, les deux states chargingRate et dischargingRate de la thingClass sunspecStorage ont minValue == -100 et maxValue == 100.

Pourquoi c'est bloquant. Avec la borne basse à 0, une consigne de charge forcée (valeur négative, convention de signe du constructeur) est structurellement inexprimable : nymea rejette le paramètre d'action avant qu'il n'atteigne le plugin.


AC-2 — cohérence lecture/écriture des consignes (anomalie A2)

onStorageBlockUpdated() alimente chargingRate depuis wChaGra() et dischargingRate depuis wDisChaGra(). Ce sont les gradients (% WChaMax/s), en lecture seule sur GEN24. Les actions, elles, écrivent dans InWRte / OutWRte (% WChaMax). Le state relu n'est donc pas la grandeur écrite : l'utilisateur écrit 50, relit tout autre chose.

Critère.

  1. chargingRate est alimenté depuis inWRte().
  2. dischargingRate est alimenté depuis outWRte().
  3. Les gradients restent exposés, mais sous des noms non ambigus : chargeGradient ← wChaGra(), dischargeGradient ← wDisChaGra(), en lecture seule.

AC-3 — réconciliation de enableDischarging (anomalie A3)

onStorageBlockUpdated() réconcilie enableCharging depuis storCtlMod().testFlag(Storctl_modCharge) mais ne fait rien pour enableDischarging. Le state reste donc figé sur sa dernière valeur écrite, et diverge silencieusement de l'onduleur dès que celui-ci change d'état seul.

Critère. enableDischarging est alimenté depuis storCtlMod().testFlag(Storctl_modDiScharge), symétriquement à enableCharging.


AC-4 — écriture de InOutWRte_RvrtTms (anomalie A4)

Le setter setInOutWRteRvrtTms() existe dans libnymea-sunspec mais n'est jamais appelé. RvrtTms est le délai au bout duquel l'onduleur abandonne la consigne et reprend son comportement natif. À 0 — le défaut — une consigne reste active indéfiniment, y compris si le HEMS s'arrête, perd le réseau ou plante.

Critère.

  1. Une valeur revertTime est configurable par thing (paramètre de la thingClass sunspecStorage), en secondes.
  2. Toute séquence de consigne (InWRte, OutWRte, StorCtlMod, ChaGriSet) se termine par une écriture de InOutWRte_RvrtTms à cette valeur.
  3. RvrtTms est écrit en dernier dans la séquence.
  4. Défaut du vendor sunspec générique : 0 (comportement amont inchangé). Défaut du vendor fronius : 300 (voir AC-10).

Plage. 0 – 28800 s.


Bloc 1 — vendor fronius, structure

AC-5 — filtre fabricant à la découverte

Critère. Pour froniusConnectionThingClassId, discoverThings() ne retient que les résultats dont au moins un modèle Common porte un nom de fabricant contenant Fronius, insensible à la casse (hasManufacturer() fait déjà un contains() insensible à la casse). Les autres résultats sont ignorés, sans créer de ThingDescriptor.

Symétriquement, un GEN24 ne doit pas être proposé deux fois : le vendor générique sunspecConnection continue de le proposer (comportement amont inchangé) ; c'est l'utilisateur qui choisit la thingClass qu'il appaire.


AC-6 — ordre des octets

Critère. Une connexion fronius utilise ByteOrderLittleEndian, c'est-à-dire le défaut SunSpec, à la découverte comme à la connexion.

Le ByteOrderBigEndian forcé du vendor solarEdge est une anomalie propre à SolarEdge et ne doit pas être étendue.

Inférence, pas fait mesuré. L'ordre des octets du GEN24 n'est documenté nulle part dans la documentation constructeur consultée ; on suppose le défaut SunSpec. À confirmer au banc (phase 2) puis sur matériel (phase 3).


AC-7 — paramètres de la thingClass froniusConnection

Critère. La thingClass froniusConnection existe, sous un bloc vendors → fronius, et porte exactement :

Paramètre Défaut Contrainte
address — comme sunspecConnection
hostName — comme sunspecConnection
macAddress — comme sunspecConnection
port 502 —
slaveId 1 fixe en TCP sur GEN24 : masqué ou en lecture seule
meterSlaveId 200 plage 0 – 247, advanced, 0 = pas de compteur sur cette connexion

Aucun paramètre endianness n'est exposé (cf. AC-6 : la valeur est imposée).


AC-8 — plancher de cadence d'interrogation

Le support constructeur recommande un intervalle d'interrogation supérieur à 7 secondes pour les applications tierces. En deçà, la surcharge de communication peut faire passer l'onduleur hors ligne vis-à-vis du portail constructeur ; le rétablissement exige une coupure AC+DC d'environ une minute sur site.

Critère.

  1. Défauts du vendor fronius : updateInterval = 10 s, timeout = 2000 ms, numberOfRetries = 3.
  2. Garde-fou dur : toute valeur d'intervalle strictement inférieure à 7 s configurée sur une connexion fronius est remontée à 7 s, avec un qCWarning explicite. Ce n'est pas une valeur par défaut modifiable : c'est un plancher dans le code.
  3. Le plancher s'applique aussi en debug.
  4. Le vendor sunspec générique et le vendor solarEdge ne sont pas affectés.

AC-9 — registres interdits en écriture

Sur GEN24, ces écritures sont acceptées puis ignorées sans code d'exception Modbus — l'échec est invisible côté HEMS : WChaMax, WChaGra, WDisChaGra, InOutWRte_WinTms, InOutWRte_RmpTms, VAChaMax (dont le scale factor n'est de surcroît pas supporté).

Critère. Sur une connexion fronius, les setters correspondants (setWChaMax, setWChaGra, setWDisChaGra, setInOutWRteWinTms, setInOutWRteRmpTms, setVAChaMax) ne sont jamais appelés. Une tentative est refusée avant émission, avec un qCWarning nommant le registre.


AC-10 — revertTime par défaut à 300 s sur fronius

Critère. La thingClass froniusConnection impose revertTime = 300 s aux things sunspecStorage créés sous elle, là où le vendor générique laisse 0 (AC-4.4).

Hypothèse de travail — décision D4 non validée. Le contrat recommande RvrtTms = 300 s à chaque consigne, avec rafraîchissement à chaque cycle d'optimisation, plutôt que le choix inverse (ne rien écrire) fait par evcc. Le raisonnement retenu : ce qui est acceptable pour un logiciel auto-hébergé ne l'est pas pour un produit installé sous la responsabilité d'ETM — une consigne persistante après un crash du HEMS immobilise la batterie du client. Prérequis non levé : la sémantique du timer est décrite dans le chapitre général du manuel et illustrée sur le modèle 123, pas sur le 124. Elle doit être vérifiée expérimentalement sur le modèle 124 avant d'être figée. Si la vérification échoue : replier sur RvrtTms = 0 + watchdog applicatif côté HEMS.


Bloc 2 — chaîne de pilotage batterie

AC-11 — correspondance intention HEMS → registres

Convention de signe du constructeur : négatif = charge, positif = décharge.

Critère. Les six modes produisent exactement cette séquence de registres :

Mode ChaGriSet StorCtl_Mod InWRte OutWRte RvrtTms
NORMAL — 0 — 100 300
NO_DISCHARGE — 2 — 0 300
NO_CHARGE — 1 0 — 300
FROZEN — 3 0 0 300
FORCE_CHARGE(p %) 1 2 — −p 300
LIMIT(c %, d %) — 3 c d 300

— = registre non écrit pour ce mode.

Ordre d'écriture imposé : ChaGriSet → StorCtl_Mod → InWRte / OutWRte → RvrtTms en dernier.

StorCtl_Mod est un bitfield : bit 0 = CHARGE, bit 1 = DISCHARGE. ChaGriSet : 0 = PV, 1 = GRID.

Les exemples du manuel constructeur affichent des fenêtres de puissance dont les signes sont incohérents avec la convention annoncée. Les valeurs de registres de ces exemples sont correctes, les fenêtres annotées ne le sont pas : la table ci-dessus est construite sur les valeurs de registres.


AC-12 — relecture du scale factor avant écriture

Critère. InOutWRte_SF est relu immédiatement avant chaque écriture de InWRte / OutWRte. Une valeur de SF mise en cache lors d'un cycle d'interrogation antérieur n'est pas réutilisée.

Motif (INV-5) : le constructeur avertit que les SF changent avec le firmware et en fonctionnement. Un SF auto-scalé entre la lecture et l'écriture produit une consigne d'un facteur 10 à côté.


AC-13 — vérification par relecture et state controlHealthy

Critère.

  1. Après chaque séquence de consigne, StorCtl_Mod, InWRte, OutWRte et ChaGriSet sont relus et comparés aux valeurs écrites.
  2. En cas de divergence : qCWarning explicite nommant le registre, la valeur écrite et la valeur relue.
  3. Un state controlHealthy (bool) passe à false en cas de divergence, à true après une séquence vérifiée conforme.

C'est la seule parade aux échecs silencieux du §5 du contrat (pilotage Modbus non autorisé, charge réseau non autorisée, restriction d'IP, assistant de tarification qui écrase les consignes).


Bloc 3 — modèle 160 (MPPT et blocs batterie)

AC-14 — identification des blocs par IDStr, jamais par index

Les blocs répétitifs du modèle 160 portent les chaînes MPPT 1, MPPT 2, StCha 3, StDisCha 4. Sur un onduleur à 3 ou 4 MPPT, les blocs batterie ne sont pas en position 3 et 4.

Critère.

  1. Le classement d'un bloc en « string PV » ou « batterie » se fait sur inputIdSting(), jamais sur l'index du bloc.
  2. Un bloc dont IDStr commence par MPPT est un string PV.
  3. Un bloc dont IDStr commence par StCha est le bloc batterie en charge ; StDisCha, le bloc batterie en décharge.
  4. Un IDStr inconnu n'est pas classé et ne contribue à aucun agrégat ; il est signalé par un qCWarning.

Le préfixe est comparé de façon insensible à la casse et après suppression des espaces de bord. Attention à l'ordre du test : StDisCha contient StCha comme sous-chaîne uniquement si on teste par contains() — le test doit donc porter sur le préfixe, et StDisCha doit être testé avant StCha si une implémentation par startsWith en cascade est retenue.


AC-15 — agrégats de puissance issus du modèle 160

DCW est un uint16, donc toujours positif : le signe est porté par le bloc actif, le bloc inactif étant à zéro.

Critère.

  1. Puissance PV = Σ DCW des blocs classés « string PV ».
  2. Puissance batterie = DCW[StDisCha] − DCW[StCha], positif = décharge.
  3. DCW_SF est partagé par tous les modules et auto-scalé : il est relu avant conversion (INV-5).
  4. Aucun calcul de production PV par différence AC/DC ± batterie. Le GEN24 expose la puissance PV réelle string par string ; répliquer le contournement du vendor solarEdge — qui existe parce que cet onduleur n'expose pas le modèle 160 — serait une régression.

Hypothèses de travail — points non tranchés

Chacun de ces points est traité selon l'hypothèse indiquée, faute de confirmation humaine au moment de l'implémentation.

# Point Statut au contrat Hypothèse retenue
H-1 Architecture multi-unit-ID (D1) ⬜ en attente de validation Option A′ : un thing froniusConnection portant deux SunSpecConnection internes (onduleur slaveId, compteur meterSlaveId). C'est l'option recommandée par le contrat : UX correcte, aucune modification de libnymea-sunspec, rebasabilité préservée.
H-2 Watchdog batterie (D4) ⬜ à valider RvrtTms = 300 s à chaque consigne (AC-10). Prérequis non levé : sémantique du timer à vérifier sur le modèle 124, pas seulement sur le 123.
H-3 Ordre des octets du GEN24 inférence explicite du brief Défaut SunSpec, little-endian (AC-6). Non documenté par le constructeur.
H-4 Deux connexions TCP simultanées vers le même onduleur inférence explicite du brief Non supposé résolu. Le constructeur recommande des requêtes séquentielles ; deux boucles d'interrogation parallèles violeraient potentiellement cette recommandation. À mesurer au banc, pas à supposer. En attendant, sérialiser.
H-5 Assistant de tarification écrasant les consignes « à confirmer », absent de la doc constructeur Constat empirique tiers. Traité en message de diagnostic (AC-13.2), pas en logique de pilotage.
H-6 Ordre des phases du chantier contrat §8 : la phase 2 ne démarre pas avant que la phase 1 (simulateur) soit verte Précondition non vérifiée ici. Le simulateur hems-sim n'a pas été exécuté. Les AC ci-dessous sont validés par tests unitaires hors ligne, ce qui ne remplace pas une validation contre un simulateur reproduisant la sémantique du constructeur.

Non vérifiable par les tests unitaires de ce dépôt

À porter en phase 2 (banc / simulateur) puis en phase 3 (matériel) :

  • l'ordre des octets réel du GEN24 (H-3) ;
  • le comportement en double connexion TCP (H-4) ;
  • l'expiration effective de RvrtTms sur le modèle 124 (H-2) ;
  • le refus d'écriture par exception Modbus quand le pilotage n'est pas autorisé côté onduleur ;
  • l'auto-scale effectif des scale factors en fonctionnement (INV-5) ;
  • le fait que les registres de AC-9 soient bien ignorés silencieusement ;
  • le comportement des blocs batterie du modèle 160 selon la version de firmware (avant/après 1.31, DCV et DCWH reportés en continu ou zérotés à l'arrêt).

Hors périmètre

  • modèles 7xx ;
  • Modbus RTU ;
  • onduleurs Verto / Tauro (le mapping MPPT diffère — c'est le motif d'AC-14, mais ils ne sont pas testés) ;
  • toute modification de libnymea-sunspec (INV-2) ;
  • tout mécanisme de compensation PV à la SolarEdge (AC-15.4) ;
  • anomalies A5 (modèle 160 absent du switch de processDiscoveryResult()), A6 (MinRsvPct non exposé) et A7 (.toInt() sur des paramètres destinés à des setters float) : réelles et documentées au contrat, mais hors des quatre correctifs demandés pour ce lot.

Tableau de statut

AC Objet Test Statut
AC-1 plage −100…+100 test_AC_1_* à renseigner
AC-2 lecture inWRte/outWRte test_AC_2_* à renseigner
AC-3 enableDischarging relu test_AC_3_* à renseigner
AC-4 RvrtTms écrit en dernier test_AC_4_* à renseigner
AC-5 filtre fabricant test_AC_5_* à renseigner
AC-6 little-endian test_AC_6_* à renseigner
AC-7 paramètres froniusConnection test_AC_7_* à renseigner
AC-8 plancher 7 s test_AC_8_* à renseigner
AC-9 registres interdits test_AC_9_* à renseigner
AC-10 revertTime 300 s test_AC_10_* à renseigner
AC-11 table mode → registres test_AC_11_* à renseigner
AC-12 relecture du SF test_AC_12_* à renseigner
AC-13 relecture + controlHealthy test_AC_13_* à renseigner
AC-14 classement par IDStr test_AC_14_* à renseigner
AC-15 agrégats modèle 160 test_AC_15_* à renseigner