etm-powersync-plugins-modbus/sunspec/CONTRAT-fronius-sunspec.md
Patrick Schurig a5b9a3b924 docs(sunspec): publier les documents de conception Fronius
Le contrat d'implémentation, la consigne de l'agent du plugin et le brief
de la phase 2 étaient exclus par le .gitignore, faute de décision sur leur
diffusion. Patrick a tranché le 2026-10-18 : le vendor Fronius est notre
plugin SunSpec, ses documents sont publics avec lui. La source support
citée a été reclassée publique le 2026-07-31 (risque R6 du contrat).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ry3vBrJT1tHdxAaXdCWi6L
2026-10-18 08:05:58 +02:00

15 KiB
Raw Blame History

Contrat d'implémentation — vendor Fronius dans nymea-plugin-sunspec

Projet ETM-PowerSync · Base nymea-plugins-modbus 1.15.2 · Cible GEN24 (Primo/Symo) + Fronius Smart Meter + batterie BYD/Reserva

Ce document est la source de vérité. Aucun agent ne code avant que la section 1 soit validée.


0. Sources et leur statut

Source Statut Usage autorisé
Manuel Fronius GEN24 Modbus TCP/RTU (public, manuals.fronius.com) Primaire Citation libre
Register maps Fronius, pack v1.2.7-2 / bundle GEN24 1.36.x Primaire Citation libre
Fronius, « GEN24 Offline — Causes and solutions » (public, fronius.com/Application Guides) — https://www.fronius.com/el-gr/~/downloads/Solar%20Energy/Application%20Guides/GEN24%20Offline%20Causes%20and%20solutions.pdf Primaire, public Citation libre
evcc (evcc-io/evcc, MIT) Secondaire, empirique Réutilisation compatible GPL
Code source nymea 1.15.2 (GPL) Primaire —

1. Décisions à valider avant tout code

D1 — Architecture multi-unit-ID ⬜ EN ATTENTE DE VALIDATION

Le GEN24 expose l'onduleur sur l'unit ID 1 (fixe en TCP) et le Smart Meter sur l'unit ID 200 par défaut (201, 202… pour les compteurs secondaires). SunSpecConnection ne porte qu'une seule slaveId.

Option Description Coût Risque
A Deux things froniusConnection, une par unit ID Nul en code UX d'appairage bancale ; deux NetworkDeviceMonitor sur la même MAC (comportement non vérifié)
A′ (recommandée) Un thing froniusConnection, deux objets SunSpecConnection en interne (onduleur slaveId=1, compteur meterSlaveId param, défaut 200) Moyen, plugin uniquement Refonte de m_sunSpecConnections (QHash ThingId→conn) et de la logique d'état connected
B Rendre slaveId multiple dans SunSpecConnection Élevé Touche du code partagé avec SolarEdge et le vendor générique → casse la rebasabilité sur l'upstream nymea

Recommandation : A′. Elle donne l'UX correcte (un seul appareil à appairer, tous les enfants sous le même parent), n'exige aucune modification de libnymea-sunspec, et préserve la capacité à rebaser le plugin sur les versions amont. Le coût réel est concentré dans createConnection(), setupThing() et processDiscoveryResult().

Prévoir un paramètre meterSlaveId (défaut 200, plage 1–247, advanced) et accepter qu'il puisse être mis à 0 = « pas de compteur sur cette connexion ».

D2 — Périmètre du vendor ✅ arrêté

Le vendor fronius n'apporte que :

  • une thingClass froniusConnection (clone de sunspecConnection + meterSlaveId, sans endianness exposé) ;
  • un filtre fabricant à la découverte sur Mn == "Fronius" ;
  • les valeurs par défaut de polling adaptées (voir I3).

Tous les enfants (onduleur, compteur, storage, MPPT) restent des thingClasses du vendor sunspec générique. Aucun équivalent de solarEdgeBattery n'est créé : la batterie Fronius est intégralement décrite par les modèles SunSpec 124 et 160.

D3 — Production PV ✅ arrêté

Aucun équivalent de calculateSolarEdgePvProduction(). Le GEN24 expose la puissance PV réelle string par string dans le modèle 160. Le bricolage AC/DC ± batterie de SolarEdge est spécifique à un onduleur qui n'expose pas le 160 ; le répliquer ici serait une régression.

D4 — Watchdog batterie ⬜ À VALIDER

InOutWRte_RvrtTms est le seul des trois timers du modèle 124 qui soit supporté et inscriptible sur GEN24 (plage 0–28800 s). À 0 — la valeur par défaut — une consigne reste active indéfiniment, y compris si le HEMS s'arrête, perd le réseau ou plante.

Recommandation : écrire RvrtTms = 300 s à chaque consigne, et rafraîchir la consigne à chaque cycle d'optimisation. L'onduleur reprend alors son comportement natif au bout de 5 minutes de silence. evcc a fait le choix inverse (ne rien écrire) ; ce choix est acceptable pour un logiciel auto-hébergé, pas pour un produit installé sous la responsabilité d'ETM.

Prérequis : la sémantique du timer est décrite dans le chapitre général du manuel et illustrée sur le modèle 123. Elle doit être vérifiée expérimentalement sur le modèle 124 en phase 2 avant d'être figée. Si la vérification échoue, replier sur RvrtTms = 0 + watchdog applicatif côté HEMS.


2. Invariants — contraintes non négociables pour tous les agents

  • I1 — Jamais d'adresse de registre en dur. Toute lecture/écriture passe par la découverte dynamique (SunSpecConnection::startDiscovery()) puis par les offsets du modèle. Le nombre de blocs MPPT (2 sans batterie, 4 avec) décale l'adresse du modèle 124 ; sans batterie le modèle 124 est absent. Corollaire : les adresses absolues du manuel (40353/40343) ne servent qu'à valider un dump, jamais à adresser. Attention à la convention : le manuel appelle « start address » le registre précédant l'ID ; les register maps donnent le registre ID. Écart systématique de 1.

  • I2 — Registres interdits en écriture sur GEN24. Ces écritures sont ignorées sans code d'exception (échec invisible) : WChaMax, WChaGra, WDisChaGra, InOutWRte_WinTms, InOutWRte_RmpTms, et VAChaMax (son scale factor est non supporté). Les setters correspondants de libnymea-sunspec doivent être neutralisés ou non appelés pour ce vendor.

  • I3 — Cadence de polling. Le support Fronius recommande un intervalle d'interrogation supérieur à 7 secondes pour les applications tierces ; au-delà, la surcharge de communication peut faire passer l'onduleur hors ligne vis-à-vis de Solar.web, et le rétablissement exige une coupure AC+DC d'environ une minute sur site. → Défauts pour le vendor Fronius : updateInterval = 10 s, timeout = 2000 ms, numberOfRetries = 3. → Requêtes séquentielles ; jamais plus de 2 en parallèle. → Ne jamais descendre sous 7 s, même en debug, sur un onduleur client.

  • I4 — Identification des blocs MPPT par IDStr, pas par index. Les chaînes sont MPPT 1, MPPT 2, StCha 3, StDisCha 4. Sur un Verto 3 ou 4 MPPT, les blocs batterie ne sont pas en position 3 et 4.

  • I5 — Les scale factors ne sont pas statiques. Fronius avertit qu'ils changent avec le firmware et en fonctionnement (auto-scale, explicite sur le modèle 160). Relire le SF concerné immédiatement avant toute écriture, et vérifier la consigne par relecture.

  • I6 — Une consigne n'est qu'une recommandation. Le manuel indique que l'onduleur peut s'en écarter pour raisons de sécurité de fonctionnement. L'optimiseur doit boucler sur la puissance batterie mesurée (modèle 160, blocs StCha/StDisCha), jamais sur la consigne émise.

  • I7 — Le contrôle peut être coupé côté onduleur, de trois façons indépendantes. Voir §5.


3. Table de vérité — modèle 124 (identique en float et en int+SF, seule l'adresse absolue diffère)

Offsets à base 1 depuis le registre ID. L = 24.

# Nom Accès Type Unité SF Plage / note
3 WChaMax R uint16 W WChaMax_SF (0) référence des % de InWRte/OutWRte
4 WChaGra R uint16 % WChaMax/s WChaDisChaGra_SF gradient — pas une consigne
5 WDisChaGra R uint16 % WChaMax/s WChaDisChaGra_SF idem
6 StorCtl_Mod RW bitfield16 — — bit 0 CHARGE, bit 1 DISCHARGE
7 VAChaMax RW uint16 VA SF non supporté ne pas écrire
8 MinRsvPct RW uint16 % −2 réserve mini de SoC
9 ChaState R uint16 % −2 SoC
10 StorAval R — — — non supporté
11 InBatV R — — — non supporté
12 ChaSt R enum16 — — 1 OFF · 2 EMPTY · 3 DISCHARGING · 4 CHARGING · 5 FULL · 6 HOLDING · 7 TESTING
13 OutWRte RW int16 % WChaMax InOutWRte_SF (−2) −100,00 … +100,00
14 InWRte RW int16 % WChaMax InOutWRte_SF (−2) −100,00 … +100,00
15 InOutWRte_WinTms R — s — non supporté
16 InOutWRte_RvrtTms RW uint16 s — 0 – 28800
17 InOutWRte_RmpTms R — s — non supporté
18 ChaGriSet RW enum16 — — 0 PV · 1 GRID

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

Les exemples 5, 6 et 7 du manuel 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. Ne pas s'en servir pour construire la table ci-dessous.

Correspondance intention HEMS → registres

Mode ETM-PowerSync ChaGriSet StorCtl_Mod InWRte OutWRte RvrtTms
NORMAL — 0 — 100 300
NO_DISCHARGE (réserve) — 2 — 0 300
NO_CHARGE (surplus au VE) — 1 0 — 300
FROZEN — 3 0 0 300
FORCE_CHARGE(p %) 1 2 — −p 300
LIMIT(c %, d %) — 3 c d 300

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


4. Modèle 160 — mapping

Bloc (IDStr) Rôle Points utiles
MPPT 1, MPPT 2 Strings PV DCW (W), DCWH (Wh cumulé), DCV, DCA
StCha 3 Batterie — charge DCW, DCWH
StDisCha 4 Batterie — décharge DCW, DCWH
  • DCW est uint16, donc toujours positif. Le signe est porté par le bloc actif : le bloc inactif est à zéro.
  • Puissance PV = Σ DCW des blocs MPPT n.
  • Puissance batterie = DCW[StDisCha] − DCW[StCha] (positif = décharge).
  • DCW_SF est partagé par tous les modules et auto-scalé. Relire avant conversion.
  • Depuis le firmware 1.31, DCV et DCWH des blocs batterie sont reportés en continu ; avant 1.31 ils étaient mis à zéro quand le bloc était inactif. DCW et DCA restent zérotés à l'arrêt dans les deux cas.

5. Prérequis onduleur — checklist de mise en service ETM

Source : Fronius, « GEN24 Offline — Causes and solutions » (public).

Trois conditions indépendantes, dont deux échouent silencieusement :

# Réglage Où Symptôme si absent
1 Allow Control (pilotage via Modbus) Interface web → Communication → Modbus Écritures sur les modèles 123/124 rejetées par exception Modbus → diagnosticable
2 « Charge de la batterie depuis le réseau » cochée Interface web → Configuration → Composants → Batterie ChaGriSet=1 sans effet, silencieux (ET logique avec le registre)
3 Restrict Control (si activée) doit inclure l'IP du boîtier ETM Interface web → Communication → Modbus (TCP seulement) Pilotage refusé, silencieux, casse au moindre changement d'IP
4 (à confirmer) ECA / Energy Cost Assistant désactivé dans Solar.web Solar.web → Réglages → Mode de fonctionnement Consignes écrasées par l'assistant, silencieux. Constat evcc, absent de la doc Fronius — à vérifier

Autres points d'installation : câble sur la prise LAN 1 (LAN 2 est réservée) ; Ethernet plutôt que WLAN ; ne pas laisser LAN et WLAN configurés simultanément ; ne pas chaîner un GEN24 à un SnapINverter/IG Plus via LAN 1/2.

Le boîtier ETM doit avoir une IP fixe (réservation DHCP), sinon la condition 3 se casse toute seule.


6. Anomalies préexistantes du plugin nymea 1.15.2

À corriger, indépendamment de Fronius. Ces bugs affectent aussi le vendor sunspec générique.

# Anomalie Emplacement Gravité
A1 chargingRate / dischargingRate bornés à [0, 100] dans le JSON → charge forcée impossible integrationpluginsunspec.json Bloquant
A2 chargingRate lu depuis wChaGra() (gradient %/s, R seul) mais écrit dans InWRte (% de WChaMax). Idem dischargingRate / wDisChaGra / OutWRte onStorageBlockUpdated() vs executeAction() Bloquant
A3 enableDischarging jamais réconcilié depuis StorCtl_Mod au block update onStorageBlockUpdated() Élevée
A4 InOutWRte_RvrtTms jamais écrit alors que le setter existe executeAction() Élevée
A5 Modèle 160 présent dans la factory mais absent du switch → tombe en default: avec un warning, aucun thing créé processDiscoveryResult() Élevée
A6 MinRsvPct (RW, SF −2) non exposé — permet de régler la réserve mini et, selon le manuel, de réveiller la batterie du mode veille — Moyenne (opportunité)
A7 .toInt() sur les paramètres d'action alors que les setters prennent des float executeAction() Moyenne

Vérifications faites et conformes, ne pas y toucher :

  • offsets de SunSpecStorageModel (InWRte = 13, RvrtTms = 15, m_fixedBlockLength = 24) ;
  • SunSpecDataPoint::isValid() couvre correctement toutes les sentinelles Fronius (0xFFFF uint16, 0x8000 int16/sunssf, 0 acc32, 0x7FC00000 float32) ;
  • tous les IDs de modèles Fronius (111/113, 201/203, 211/213, 120–124, 160) sont reconnus par la factory.

7. Registre des risques

# Risque Impact Mitigation
R1 Polling trop rapide → onduleur hors ligne pour Solar.web chez un client Déplacement sur site pour coupure AC+DC I3 : plancher dur à 7 s, défaut à 10 s, garde-fou dans le code
R2 Consigne persistante après crash du HEMS (RvrtTms = 0) Batterie bloquée, facture client D4 : RvrtTms = 300 s
R3 Échec silencieux du pilotage (conditions 2, 3, 4 du §5) HEMS qui croit piloter Vérification par relecture après écriture + alerte
R4 SF auto-scalé entre lecture et écriture Consigne d'un facteur 10 à côté I5 : relecture du SF avant écriture
R5 Divergence avec l'upstream nymea Rebase impossible D1 = option A′ : aucune modification de libnymea-sunspec
R6 Fuite du document support confidentiel dans un repo public — source reclassée publique le 2026-07-31, voir §0 — Résolu

8. Roadmap

Phase Objet Repo Sortie
1 Mise à jour du simulateur SunSpec hems-sim Simulateur reproduisant fidèlement le comportement GEN24
2 Vendor Fronius + corrections A1–A7 nymea-plugin-sunspec Paquet powersync-plugin-sunspec en canal testing
3 Test sur un HEMS client existant — Protocole d'essai signé, go/no-go

La phase 2 ne démarre pas avant que la phase 1 soit verte. Un plugin testé contre un simulateur qui n'implémente pas la sémantique Fronius validerait du code qui échouera chez le client — c'est précisément le mode de défaillance que ce contrat cherche à éviter.

Briefs d'agents : brief-agent-simulateur.md, brief-agent-plugin.md, protocole-test-client.md.