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
15 KiB
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 desunspecConnection+meterSlaveId, sansendiannessexposé) ; - 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, etVAChaMax(son scale factor est non supporté). Les setters correspondants delibnymea-sunspecdoivent ê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 sontMPPT 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 |
DCWest uint16, donc toujours positif. Le signe est porté par le bloc actif : le bloc inactif est à zéro.- Puissance PV = Σ
DCWdes blocsMPPT n. - Puissance batterie =
DCW[StDisCha]−DCW[StCha](positif = décharge). DCW_SFest partagé par tous les modules et auto-scalé. Relire avant conversion.- Depuis le firmware 1.31,
DCVetDCWHdes blocs batterie sont reportés en continu ; avant 1.31 ils étaient mis à zéro quand le bloc était inactif.DCWetDCArestent 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 | — | 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.