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 |