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
This commit is contained in:
Patrick Schurig 2026-10-18 08:05:58 +02:00
parent 4d17691239
commit a5b9a3b924
2 changed files with 206 additions and 7 deletions

7
.gitignore vendored
View File

@ -12,12 +12,5 @@ build-*/
# systeme). Artefact de build, jamais versionne.
.sunspec-sysroot/
# Documents de conception du chantier Fronius : ils vivent dans hems-system et
# sont copies ici pour travailler. Leur diffusion publique n'est pas tranchee
# (cf. section 5 du contrat), donc ils ne sont pas versionnes dans ce depot.
sunspec/CONTRAT-fronius-sunspec.md
sunspec/brief-agent-plugin.md
sunspec/CLAUDE-nymea-plugin-sunspec.md
# Fichier de session Qt Creator : chemins absolus locaux, propre a chaque poste.
*.pro.user

View File

@ -0,0 +1,206 @@
# 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`.