From a5b9a3b9249a934ec3f90dedb146efab9f1be593 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sun, 18 Oct 2026 08:05:58 +0200 Subject: [PATCH] docs(sunspec): publier les documents de conception Fronius MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Ry3vBrJT1tHdxAaXdCWi6L --- .gitignore | 7 - sunspec/CONTRAT-fronius-sunspec.md | 206 +++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 sunspec/CONTRAT-fronius-sunspec.md diff --git a/.gitignore b/.gitignore index 2eea77d..b7ac4b1 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/sunspec/CONTRAT-fronius-sunspec.md b/sunspec/CONTRAT-fronius-sunspec.md new file mode 100644 index 0000000..eb47ab4 --- /dev/null +++ b/sunspec/CONTRAT-fronius-sunspec.md @@ -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`.