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

207 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.