From 93ecfb4ca1c1d426dd7764c66a82f3b56d5edd6b Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Fri, 31 Jul 2026 10:16:06 +0200 Subject: [PATCH] specs: add acceptance criteria for Fronius SunSpec support AC-1..AC-4 are the four upstream anomalies of the generic sunspec vendor (setpoint range, read/write mismatch on the rate states, enableDischarging never reconciled, revert timer never written). AC-5..AC-15 are the Fronius-specific behaviour: manufacturer filter, byte order, connection parameters, polling floor, forbidden writes, the HEMS intent to register table, scale factor reread, readback verification and the model 160 block classification. Points the design contract leaves open are recorded as explicit working hypotheses (H-1..H-6) rather than blocking on them. --- specs/fronius-sunspec.md | 378 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 specs/fronius-sunspec.md diff --git a/specs/fronius-sunspec.md b/specs/fronius-sunspec.md new file mode 100644 index 0000000..1ac1494 --- /dev/null +++ b/specs/fronius-sunspec.md @@ -0,0 +1,378 @@ +# Spec — support Fronius du plugin SunSpec + +Critères d'acceptation numérotés, dérivés du contrat de conception Fronius +(document interne, non versionné ici) et du brief de lot 0. + +Deux natures de critères, à ne jamais mélanger dans un même commit : + +| Bloc | Nature | Commits | +|---|---|---| +| **AC-1 … AC-4** | correctifs d'anomalies présentes dans nymea 1.15.2 amont | message en anglais, aucune référence ETM / Fronius / PowerSync — ils ont vocation à être proposés en amont | +| **AC-5 … AC-15** | spécifique ETM (vendor `fronius`) | commits séparés, préfixe libre | + +Statut des critères : voir `## Tableau de statut` en fin de document. + +--- + +## Invariants applicables à toute l'implémentation + +Ces règles ne sont pas des critères testables un à un ; elles conditionnent +chaque AC ci-dessous. + +- **INV-1 — aucune adresse de registre en dur.** Toute lecture et toute écriture + passent par la découverte dynamique puis par les offsets du modèle. Le nombre + de blocs répétitifs du modèle 160 décale l'adresse absolue du modèle 124, et + sans batterie le modèle 124 est absent : une adresse absolue n'est jamais + valide durablement. Les adresses des documentations constructeur servent à + valider un dump, jamais à adresser. +- **INV-2 — `libnymea-sunspec` n'est jamais modifiée.** Tout vit dans + `sunspec/`. Voir `sunspec/VENDORED.md`. +- **INV-3 — une consigne écrite est relue.** Plusieurs onduleurs acceptent une + écriture puis l'ignorent sans code d'exception Modbus. +- **INV-4 — la régulation se ferme sur la puissance mesurée**, jamais sur la + consigne émise. +- **INV-5 — les scale factors ne sont pas statiques.** Ils changent avec le + firmware et en fonctionnement (auto-scale). Relire le SF concerné + immédiatement avant conversion et avant écriture. + +--- + +## Bloc 0 — correctifs upstream + +### AC-1 — plage des consignes de charge/décharge (anomalie A1) + +`chargingRate` et `dischargingRate` sont déclarés `minValue: 0, maxValue: 100` +dans `integrationpluginsunspec.json`. La plage réelle des points `InWRte` / +`OutWRte` du modèle 124 est **−100,00 … +100,00** (`int16`, SF −2). + +**Critère.** Dans `integrationpluginsunspec.json`, les deux states +`chargingRate` et `dischargingRate` de la thingClass `sunspecStorage` ont +`minValue == -100` et `maxValue == 100`. + +**Pourquoi c'est bloquant.** Avec la borne basse à 0, une consigne de charge +forcée (valeur négative, convention de signe du constructeur) est structurellement +inexprimable : nymea rejette le paramètre d'action avant qu'il n'atteigne le +plugin. + +--- + +### AC-2 — cohérence lecture/écriture des consignes (anomalie A2) + +`onStorageBlockUpdated()` alimente `chargingRate` depuis `wChaGra()` et +`dischargingRate` depuis `wDisChaGra()`. Ce sont les **gradients** +(`% WChaMax/s`), en lecture seule sur GEN24. Les actions, elles, écrivent dans +`InWRte` / `OutWRte` (`% WChaMax`). Le state relu n'est donc pas la grandeur +écrite : l'utilisateur écrit 50, relit tout autre chose. + +**Critère.** +1. `chargingRate` est alimenté depuis `inWRte()`. +2. `dischargingRate` est alimenté depuis `outWRte()`. +3. Les gradients restent exposés, mais sous des noms non ambigus : + `chargeGradient` ← `wChaGra()`, `dischargeGradient` ← `wDisChaGra()`, + en lecture seule. + +--- + +### AC-3 — réconciliation de `enableDischarging` (anomalie A3) + +`onStorageBlockUpdated()` réconcilie `enableCharging` depuis +`storCtlMod().testFlag(Storctl_modCharge)` mais ne fait rien pour +`enableDischarging`. Le state reste donc figé sur sa dernière valeur écrite, et +diverge silencieusement de l'onduleur dès que celui-ci change d'état seul. + +**Critère.** `enableDischarging` est alimenté depuis +`storCtlMod().testFlag(Storctl_modDiScharge)`, symétriquement à +`enableCharging`. + +--- + +### AC-4 — écriture de `InOutWRte_RvrtTms` (anomalie A4) + +Le setter `setInOutWRteRvrtTms()` existe dans `libnymea-sunspec` mais n'est +jamais appelé. `RvrtTms` est le délai au bout duquel l'onduleur abandonne la +consigne et reprend son comportement natif. À 0 — le défaut — une consigne reste +active indéfiniment, y compris si le HEMS s'arrête, perd le réseau ou plante. + +**Critère.** +1. Une valeur `revertTime` est configurable par thing (paramètre de la + thingClass `sunspecStorage`), en secondes. +2. Toute séquence de consigne (`InWRte`, `OutWRte`, `StorCtlMod`, `ChaGriSet`) + se termine par une écriture de `InOutWRte_RvrtTms` à cette valeur. +3. `RvrtTms` est écrit **en dernier** dans la séquence. +4. Défaut du vendor `sunspec` générique : **0** (comportement amont inchangé). + Défaut du vendor `fronius` : **300** (voir AC-10). + +**Plage.** 0 – 28800 s. + +--- + +## Bloc 1 — vendor `fronius`, structure + +### AC-5 — filtre fabricant à la découverte + +**Critère.** Pour `froniusConnectionThingClassId`, `discoverThings()` ne retient +que les résultats dont au moins un modèle Common porte un nom de fabricant +contenant `Fronius`, insensible à la casse (`hasManufacturer()` fait déjà un +`contains()` insensible à la casse). Les autres résultats sont ignorés, sans +créer de `ThingDescriptor`. + +Symétriquement, un GEN24 ne doit pas être proposé deux fois : le vendor +générique `sunspecConnection` continue de le proposer (comportement amont +inchangé) ; c'est l'utilisateur qui choisit la thingClass qu'il appaire. + +--- + +### AC-6 — ordre des octets + +**Critère.** Une connexion `fronius` utilise `ByteOrderLittleEndian`, c'est-à-dire +le défaut SunSpec, à la découverte comme à la connexion. + +Le `ByteOrderBigEndian` forcé du vendor `solarEdge` est une anomalie propre à +SolarEdge et ne doit pas être étendue. + +> **Inférence, pas fait mesuré.** L'ordre des octets du GEN24 n'est documenté +> nulle part dans la documentation constructeur consultée ; on suppose le défaut +> SunSpec. À confirmer au banc (phase 2) puis sur matériel (phase 3). + +--- + +### AC-7 — paramètres de la thingClass `froniusConnection` + +**Critère.** La thingClass `froniusConnection` existe, sous un bloc `vendors` +→ `fronius`, et porte exactement : + +| Paramètre | Défaut | Contrainte | +|---|---|---| +| `address` | — | comme `sunspecConnection` | +| `hostName` | — | comme `sunspecConnection` | +| `macAddress` | — | comme `sunspecConnection` | +| `port` | 502 | — | +| `slaveId` | **1** | fixe en TCP sur GEN24 : masqué ou en lecture seule | +| `meterSlaveId` | **200** | plage 0 – 247, `advanced`, **0 = pas de compteur sur cette connexion** | + +Aucun paramètre `endianness` n'est exposé (cf. AC-6 : la valeur est imposée). + +--- + +### AC-8 — plancher de cadence d'interrogation + +Le support constructeur recommande un intervalle d'interrogation supérieur à +**7 secondes** pour les applications tierces. En deçà, la surcharge de +communication peut faire passer l'onduleur hors ligne vis-à-vis du portail +constructeur ; le rétablissement exige une coupure AC+DC d'environ une minute +**sur site**. + +**Critère.** +1. Défauts du vendor `fronius` : `updateInterval = 10` s, `timeout = 2000` ms, + `numberOfRetries = 3`. +2. **Garde-fou dur** : toute valeur d'intervalle strictement inférieure à 7 s + configurée sur une connexion `fronius` est remontée à 7 s, avec un + `qCWarning` explicite. Ce n'est pas une valeur par défaut modifiable : c'est + un plancher dans le code. +3. Le plancher s'applique aussi en debug. +4. Le vendor `sunspec` générique et le vendor `solarEdge` ne sont pas affectés. + +--- + +### AC-9 — registres interdits en écriture + +Sur GEN24, ces écritures sont acceptées puis **ignorées sans code d'exception +Modbus** — l'échec est invisible côté HEMS : +`WChaMax`, `WChaGra`, `WDisChaGra`, `InOutWRte_WinTms`, `InOutWRte_RmpTms`, +`VAChaMax` (dont le scale factor n'est de surcroît pas supporté). + +**Critère.** Sur une connexion `fronius`, les setters correspondants +(`setWChaMax`, `setWChaGra`, `setWDisChaGra`, `setInOutWRteWinTms`, +`setInOutWRteRmpTms`, `setVAChaMax`) ne sont **jamais** appelés. Une tentative +est refusée avant émission, avec un `qCWarning` nommant le registre. + +--- + +### AC-10 — `revertTime` par défaut à 300 s sur `fronius` + +**Critère.** La thingClass `froniusConnection` impose `revertTime = 300` s aux +things `sunspecStorage` créés sous elle, là où le vendor générique laisse 0 +(AC-4.4). + +> **Hypothèse de travail — décision D4 non validée.** Le contrat recommande +> `RvrtTms = 300 s` à chaque consigne, avec rafraîchissement à chaque cycle +> d'optimisation, plutôt que le choix inverse (ne rien écrire) fait par evcc. +> Le raisonnement retenu : ce qui est acceptable pour un logiciel auto-hébergé +> ne l'est pas pour un produit installé sous la responsabilité d'ETM — une +> consigne persistante après un crash du HEMS immobilise la batterie du client. +> **Prérequis non levé :** la sémantique du timer est décrite dans le chapitre +> général du manuel et illustrée sur le modèle 123, pas sur le 124. Elle doit +> être vérifiée expérimentalement sur le modèle 124 avant d'être figée. Si la +> vérification échoue : replier sur `RvrtTms = 0` + watchdog applicatif côté +> HEMS. + +--- + +## Bloc 2 — chaîne de pilotage batterie + +### AC-11 — correspondance intention HEMS → registres + +Convention de signe du constructeur : **négatif = charge, positif = décharge**. + +**Critère.** Les six modes produisent exactement cette séquence de registres : + +| Mode | `ChaGriSet` | `StorCtl_Mod` | `InWRte` | `OutWRte` | `RvrtTms` | +|---|---|---|---|---|---| +| `NORMAL` | — | 0 | — | 100 | 300 | +| `NO_DISCHARGE` | — | 2 | — | 0 | 300 | +| `NO_CHARGE` | — | 1 | 0 | — | 300 | +| `FROZEN` | — | 3 | 0 | 0 | 300 | +| `FORCE_CHARGE(p %)` | 1 | 2 | — | **−p** | 300 | +| `LIMIT(c %, d %)` | — | 3 | c | d | 300 | + +`—` = registre non écrit pour ce mode. + +**Ordre d'écriture imposé :** `ChaGriSet` → `StorCtl_Mod` → `InWRte` / `OutWRte` +→ `RvrtTms` en dernier. + +`StorCtl_Mod` est un bitfield : bit 0 = CHARGE, bit 1 = DISCHARGE. +`ChaGriSet` : 0 = PV, 1 = GRID. + +> Les exemples du manuel constructeur 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 : la table ci-dessus est construite sur les valeurs de registres. + +--- + +### AC-12 — relecture du scale factor avant écriture + +**Critère.** `InOutWRte_SF` est relu immédiatement avant chaque écriture de +`InWRte` / `OutWRte`. Une valeur de SF mise en cache lors d'un cycle +d'interrogation antérieur n'est pas réutilisée. + +Motif (INV-5) : le constructeur avertit que les SF changent avec le firmware +**et en fonctionnement**. Un SF auto-scalé entre la lecture et l'écriture +produit une consigne d'un facteur 10 à côté. + +--- + +### AC-13 — vérification par relecture et state `controlHealthy` + +**Critère.** +1. Après chaque séquence de consigne, `StorCtl_Mod`, `InWRte`, `OutWRte` et + `ChaGriSet` sont relus et comparés aux valeurs écrites. +2. En cas de divergence : `qCWarning` explicite nommant le registre, la valeur + écrite et la valeur relue. +3. Un state `controlHealthy` (bool) passe à `false` en cas de divergence, à + `true` après une séquence vérifiée conforme. + +C'est la seule parade aux échecs silencieux du §5 du contrat (pilotage Modbus +non autorisé, charge réseau non autorisée, restriction d'IP, assistant de +tarification qui écrase les consignes). + +--- + +## Bloc 3 — modèle 160 (MPPT et blocs batterie) + +### AC-14 — identification des blocs par `IDStr`, jamais par index + +Les blocs répétitifs du modèle 160 portent les chaînes `MPPT 1`, `MPPT 2`, +`StCha 3`, `StDisCha 4`. Sur un onduleur à 3 ou 4 MPPT, les blocs batterie ne +sont **pas** en position 3 et 4. + +**Critère.** +1. Le classement d'un bloc en « string PV » ou « batterie » se fait sur + `inputIdSting()`, jamais sur l'index du bloc. +2. Un bloc dont `IDStr` commence par `MPPT` est un string PV. +3. Un bloc dont `IDStr` commence par `StCha` est le bloc batterie en charge ; + `StDisCha`, le bloc batterie en décharge. +4. Un `IDStr` inconnu n'est pas classé et ne contribue à aucun agrégat ; il est + signalé par un `qCWarning`. + +Le préfixe est comparé de façon insensible à la casse et après suppression des +espaces de bord. **Attention à l'ordre du test** : `StDisCha` contient +`StCha` comme sous-chaîne uniquement si on teste par `contains()` — le test doit +donc porter sur le préfixe, et `StDisCha` doit être testé avant `StCha` si une +implémentation par `startsWith` en cascade est retenue. + +--- + +### AC-15 — agrégats de puissance issus du modèle 160 + +`DCW` est un `uint16`, donc toujours positif : le signe est porté par le bloc +actif, le bloc inactif étant à zéro. + +**Critère.** +1. Puissance PV = Σ `DCW` des blocs classés « string PV ». +2. Puissance batterie = `DCW[StDisCha]` − `DCW[StCha]`, **positif = décharge**. +3. `DCW_SF` est partagé par tous les modules et auto-scalé : il est relu avant + conversion (INV-5). +4. **Aucun calcul de production PV par différence AC/DC ± batterie.** Le GEN24 + expose la puissance PV réelle string par string ; répliquer le contournement + du vendor `solarEdge` — qui existe parce que cet onduleur n'expose pas le + modèle 160 — serait une régression. + +--- + +## Hypothèses de travail — points non tranchés + +Chacun de ces points est traité selon l'hypothèse indiquée, faute de +confirmation humaine au moment de l'implémentation. + +| # | Point | Statut au contrat | Hypothèse retenue | +|---|---|---|---| +| H-1 | Architecture multi-unit-ID (D1) | ⬜ en attente de validation | **Option A′** : un thing `froniusConnection` portant deux `SunSpecConnection` internes (onduleur `slaveId`, compteur `meterSlaveId`). C'est l'option recommandée par le contrat : UX correcte, aucune modification de `libnymea-sunspec`, rebasabilité préservée. | +| H-2 | Watchdog batterie (D4) | ⬜ à valider | **`RvrtTms = 300 s`** à chaque consigne (AC-10). Prérequis non levé : sémantique du timer à vérifier sur le modèle 124, pas seulement sur le 123. | +| H-3 | Ordre des octets du GEN24 | inférence explicite du brief | Défaut SunSpec, little-endian (AC-6). Non documenté par le constructeur. | +| H-4 | Deux connexions TCP simultanées vers le même onduleur | inférence explicite du brief | Non supposé résolu. Le constructeur recommande des requêtes séquentielles ; deux boucles d'interrogation parallèles violeraient potentiellement cette recommandation. **À mesurer au banc**, pas à supposer. En attendant, sérialiser. | +| H-5 | Assistant de tarification écrasant les consignes | « à confirmer », absent de la doc constructeur | Constat empirique tiers. Traité en message de diagnostic (AC-13.2), pas en logique de pilotage. | +| H-6 | Ordre des phases du chantier | contrat §8 : la phase 2 ne démarre pas avant que la phase 1 (simulateur) soit verte | **Précondition non vérifiée ici.** Le simulateur `hems-sim` n'a pas été exécuté. Les AC ci-dessous sont validés par tests unitaires hors ligne, ce qui ne remplace pas une validation contre un simulateur reproduisant la sémantique du constructeur. | + +--- + +## Non vérifiable par les tests unitaires de ce dépôt + +À porter en phase 2 (banc / simulateur) puis en phase 3 (matériel) : + +- l'ordre des octets réel du GEN24 (H-3) ; +- le comportement en double connexion TCP (H-4) ; +- l'expiration effective de `RvrtTms` sur le modèle 124 (H-2) ; +- le refus d'écriture par exception Modbus quand le pilotage n'est pas autorisé + côté onduleur ; +- l'auto-scale effectif des scale factors en fonctionnement (INV-5) ; +- le fait que les registres de AC-9 soient bien ignorés silencieusement ; +- le comportement des blocs batterie du modèle 160 selon la version de firmware + (avant/après 1.31, `DCV` et `DCWH` reportés en continu ou zérotés à l'arrêt). + +--- + +## Hors périmètre + +- modèles 7xx ; +- Modbus RTU ; +- onduleurs Verto / Tauro (le mapping MPPT diffère — c'est le motif d'AC-14, + mais ils ne sont pas testés) ; +- toute modification de `libnymea-sunspec` (INV-2) ; +- tout mécanisme de compensation PV à la SolarEdge (AC-15.4) ; +- anomalies A5 (modèle 160 absent du `switch` de `processDiscoveryResult()`), + A6 (`MinRsvPct` non exposé) et A7 (`.toInt()` sur des paramètres destinés à + des setters `float`) : réelles et documentées au contrat, mais hors des quatre + correctifs demandés pour ce lot. + +--- + +## Tableau de statut + +| AC | Objet | Test | Statut | +|---|---|---|---| +| AC-1 | plage −100…+100 | `test_AC_1_*` | à renseigner | +| AC-2 | lecture `inWRte`/`outWRte` | `test_AC_2_*` | à renseigner | +| AC-3 | `enableDischarging` relu | `test_AC_3_*` | à renseigner | +| AC-4 | `RvrtTms` écrit en dernier | `test_AC_4_*` | à renseigner | +| AC-5 | filtre fabricant | `test_AC_5_*` | à renseigner | +| AC-6 | little-endian | `test_AC_6_*` | à renseigner | +| AC-7 | paramètres `froniusConnection` | `test_AC_7_*` | à renseigner | +| AC-8 | plancher 7 s | `test_AC_8_*` | à renseigner | +| AC-9 | registres interdits | `test_AC_9_*` | à renseigner | +| AC-10 | `revertTime` 300 s | `test_AC_10_*` | à renseigner | +| AC-11 | table mode → registres | `test_AC_11_*` | à renseigner | +| AC-12 | relecture du SF | `test_AC_12_*` | à renseigner | +| AC-13 | relecture + `controlHealthy` | `test_AC_13_*` | à renseigner | +| AC-14 | classement par `IDStr` | `test_AC_14_*` | à renseigner | +| AC-15 | agrégats modèle 160 | `test_AC_15_*` | à renseigner |