# AGENTS.md — plugin Eastron (compteurs SDM)

> Repo : `etm-powersync-plugins-modbus` · Dossier : `eastron/` · Branche : `main`
> Compteurs d'énergie **Eastron SDM** (SDM72, SDM120, SDM220, SDM230, SDM630).
> Comm : **Modbus RTU / RS485 uniquement** (pas de variante TCP dans ce plugin).
> Statut cible : **Supporté / prod** — SDM72 (principal) + SDM120 (PV) en prod sur `etm-nymea-dev`.
>   Paquet : `eastron 1.15.0+etm3` (amd64 + cross arm64, canaux nightly/testing/stable).
> Plugin id : `2078c2fc-c4cd-46bb-95ab-e9a55ef0a281` · Vendor `eastron` `33529c0d-67e6-441e-b0bb-6e76e9d4bc1c`.
>
> **Nature du plugin (à comprendre AVANT toute modif)** : c'est un plugin **généré par le
> modbus-tool de nymea**, pas un plugin écrit à la main. La carte de registres et le décodage
> sont **déclaratifs** (`*-registers.json`) ; le `.cpp`/`.h` est en grande partie du code
> généré + un peu de glue (discovery, setup, câblage des states). On édite le JSON, **pas** les
> classes de connexion générées.
>
> **Sources de vérité (lues juin 2026, commit `b1da668`)** :
> - `sdm72-registers.json` — carte SDM72 (triphasé). ✅ lue intégralement.
> - `sdm120-registers.json` — carte SDM120 (monophasé). ✅ lue intégralement.
> - `integrationplugineastron.json` — ThingClasses, interfaces, params, states. ✅ lue
>   (tronquée en toute fin sur `sdm230Producer`, mais les 5 familles de modèles sont vues).
> - **Protocoles constructeur (datasheets Eastron, lus juin 2026)** : SDM120-Modbus V2.4,
>   SDM230-Modbus V1.2, SDM72D-M-2 V1.1.1 (2023). ✅ croisés avec les `*-registers.json` —
>   adresses, FC04, endianness confirmées (voir §4).
> - **Non encore relus dans ce brief** : `integrationplugineastron.cpp`/`.h` (discovery + glue),
>   `eastron.pro`, `meta.json`, `sdm630-registers.json`, `sdm220-registers.json`,
>   `sdm230-registers.json` (contenu JSON ; les adresses SDM230 sont confirmées par le
>   datasheet). → voir « À vérifier dans le code » en fin de fichier.

---

## 1. RÔLE

Exposer des compteurs Eastron SDM lus en Modbus RTU comme Things nymea implémentant les
interfaces énergie (`energymeter` / `smartmeterconsumer` / `smartmeterproducer`), consommées par
le moteur `energy-etm` (`EnergyArbitrator`) pour le bilan de puissance/énergie (point de
livraison, production PV).

**Read-only.** Un compteur ne se pilote pas : aucune action, aucun registre en écriture, aucun
`paramRegisters` d'écriture. Tout est `inputRegister` `access: RO`. Toute tentative d'ajouter une
écriture est hors périmètre (et faux pour ce matériel).

## 2. ARCHITECTURE — la séparation à ne jamais oublier

Le point central de ce plugin (équivalent de la « règle d'or transport » du brief V2C, mais
inversé) : **le plugin ne possède PAS le port série.**

```
nymead
 └─ ModbusRtuMaster  (objet nymea, configuré HORS plugin)
      • /dev/ttyUSB0  ← le port physique appartient au master, pas au plugin
      • 9600 8N1, timeout, retries  ← configurés sur le MASTER
      • un seul master = un seul maître sur le bus RS485
        └─ Thing « SDM72 »   (slaveAddress 1)  ─┐  partagent le même
        └─ Thing « SDM120 »  (slaveAddress 2)  ─┘  master via modbusMasterUuid
```

- Chaque ThingClass a deux `paramTypes` clés :
  - `slaveAddress` (`uint`, défaut 1) — l'unit ID Modbus du compteur sur le bus.
  - `modbusMasterUuid` (`QUuid`, **readOnly**) — pointe vers le `ModbusRtuMaster` nymea qui
    porte le port série. Renseigné par le mécanisme de setup/discovery, jamais saisi à la main.
- 🔴 **Conséquence directe** : le baud (9600), la parité (N), le timeout (500 ms) et les retries
  (3) **ne sont PAS dans ce plugin**. Ils vivent dans la config du `ModbusRtuMaster` (côté
  nymea-app → Modbus RTU, ou config nymead). Chercher un bug de baud/timeout dans `eastron/` est
  une perte de temps : c'est au niveau du master. *(Localisation exacte du réglage à confirmer
  selon la version de nymea-app, mais ce n'est pas dans ce dossier.)*
- 🔴 **Bus mono-maître** : tous les SDM partagent le même master = le même `/dev/ttyUSB0`.
  nymead est le **seul** maître. Voir §7 (discipline de bus) — c'est la cause racine n°1 des
  timeouts intermittents.

## 3. THINGCLASSES, RÔLES ÉNERGIE & CONVENTION ETM

### 3.1 Matrice complète du plugin (ce qui existe)

5 modèles × 3 variantes de rôle = 15 ThingClasses (toutes en `createMethods: ["discovery"]`,
toutes avec `connectable` + une interface énergie selon la variante) :

| Modèle | « — Energy Meter » | « — Producer Meter » | « — Consumer Meter » |
|---|---|---|---|
| **SDM72** (3φ)  | `sdm72`  (`energymeter`)  | `sdm72Producer`  (`smartmeterproducer`)  | `sdm72Consumer`  (`smartmeterconsumer`)  |
| **SDM120** (1φ) | `sdm120` (`energymeter`)  | `sdm120Producer` (`smartmeterproducer`)  | `sdm120Consumer` (`smartmeterconsumer`)  |
| **SDM220** (1φ) | `sdm220` (`energymeter`)  | `sdm220Producer` (`smartmeterproducer`)  | `sdm220Consumer` (`smartmeterconsumer`)  |
| **SDM230** (1φ) | `sdm230` (`energymeter`)  | `sdm230Producer` (`smartmeterproducer`)  | `sdm230Consumer` (`smartmeterconsumer`)  |
| **SDM630** (3φ) | `sdm630` (`energymeter`)  | `sdm630Producer` (`smartmeterproducer`)  | `sdm630Consumer` (`smartmeterconsumer`)  |

States par variante : **Energy Meter** = riche (tension/courant/puissance ± par phase +
`totalEnergyConsumed` + `totalEnergyProduced`) ; **Producer** = `currentPower`,
`totalEnergyProduced`, `frequency` ; **Consumer** = `currentPower`, `totalEnergyConsumed`,
`frequency`. Toutes ont `connected` (`bool`, `cached:false`).

### 3.2 Comment nymea attribue le rôle (source `energymanagerimpl.cpp`)

🔴 **Le rôle énergie d'un compteur = son INTERFACE, lue statiquement par l'`EnergyManager`.** Il
n'y a aucun paramètre runtime « rôle » côté plugin. La logique du manager :

- **Root meter (grid)** : `setRootMeter()` **refuse** un thing qui n'a pas l'interface
  `energymeter`. Le compteur réseau *doit* être un `energymeter`, et il n'y en a **qu'un**. Son
  `currentPower` signé (+ = consommé / − = produit) et ses deux totaux donnent l'échange réseau
  dans les deux sens → **un seul thing couvre import ET export**.
- **Producteurs** : production = `configuredThings().filterByInterface("smartmeterproducer")`.
  Seuls les things `smartmeterproducer` comptent comme production (onduleur/PV).
- **Consommateurs** : `filterByInterface("smartmeterconsumer")` (charge dédiée, PAC…).
- ⚠️ **Gotcha auto-root** : si aucun root meter n'est défini, le manager **promeut
  automatiquement le premier `energymeter` qui apparaît**. Ne pas laisser traîner un `energymeter`
  parasite qui raflerait le rôle de root à la place du bon compteur.

🔴 **Pourquoi on ne peut PAS fusionner en une seule ThingClass par modèle** : un thing portant à
la fois `energymeter` + `smartmeterproducer` + `smartmeterconsumer` serait ramassé par **les deux**
boucles `filterByInterface` → **double comptage** du bilan (compté en prod ET en conso). Le modèle
interface-driven impose donc une ThingClass par rôle. C'est une limite de l'**energy experience**,
pas du plugin `eastron`.

### 3.3 Le rôle dépend de l'INSTALLATION, pas du modèle (convention ETM)

Le même modèle sert à des rôles différents selon le chantier (ex. onduleur 3 kW → SDM120,
onduleur 10 kW → SDM230 ; en triphasé SDM72/SDM630 en compteur réseau, onduleur ou conso PAC).
La matrice modèle×rôle est donc **irréductible** : aucune variante n'est « jamais utilisée », **ne
pas élaguer** (on casserait des cas réels). Couverture ETM des (modèle × rôle) effectivement
déployés :

| Modèle | Root meter (`energymeter`) | Producteur (`smartmeterproducer`) | Consommateur (`smartmeterconsumer`) |
|---|:---:|:---:|:---:|
| **SDM72** (3φ)  | ✅ | ✅ | ✅ |
| **SDM630** (3φ) | ✅ | ✅¹ | ✅¹ |
| **SDM230** (1φ) | ✅ | ✅ | ✅ |
| **SDM120** (1φ) | —² | ✅ | ✅ |
| **SDM220** (1φ) | —³ | —³ | —³ |

¹ SDM630 en producteur/consommateur = gros onduleur ou PAC **triphasés** (selon la remarque
  triphasé : SDM72/SDM630 couvrent les trois rôles en 3φ).
² SDM120 n'est pas déployé comme compteur réseau (trop petit / dédié branche). La ThingClass
  `sdm120` (`energymeter`) **existe** et serait techniquement acceptée comme root par nymea, mais
  hors convention ETM — et attention au gotcha auto-root.
³ SDM220 : présent dans le plugin, **hors convention de déploiement ETM listée** (à traiter comme
  les autres 1φ si un cas se présente).

### 3.4 Convention `displayName` par fonction (action court terme, ZÉRO risque)

La liste « ajouter un thing » montre les ThingClasses par `displayName`. Renommer **uniquement le
`displayName`** (jamais l'`id` ni le `name`) ne casse **aucun** thing appairé en prod — c'est
l'action sûre et disponible tout de suite pour rendre la liste lisible (l'installateur choisit par
**fonction**, pas par « Producer/Consumer/Energy Meter »). Convention ETM proposée :

| ThingClass (`name`) | `displayName` proposé |
|---|---|
| `sdm72` / `sdm230` / `sdm630` | **Compteur réseau — Eastron SDMxx (Nφ)** |
| `sdm72Producer` / `sdm230Producer` / `sdm120Producer` / `sdm630Producer` | **Onduleur / PV — Eastron SDMxx (Nφ)** |
| `sdm72Consumer` / `sdm230Consumer` / `sdm120Consumer` / `sdm630Consumer` | **Consommateur (PAC/charge) — Eastron SDMxx (Nφ)** |

Édition à faire dans `integrationplugineastron.json` (champ `displayName` de chaque ThingClass).
Regrouper les libellés par rôle aide à scanner les 15 entrées en 3 blocs. Ça **ne réduit pas** le
nombre d'entrées (15 restent 15) — ça les rend intentionnelles.

### 3.5 Le vrai fix de fond (hors périmètre plugin — feature request nymea)

Réduire à « 1 entrée par modèle, rôle choisi après » suppose de modifier
`nymea-experience-plugin-energy`, **pas** ce plugin. **Demande à porter à l'équipe nymea** :

> Permettre d'assigner le rôle énergie (root/grid, producteur, consommateur) à un seul thing
> `energymeter` bidirectionnel depuis l'energy experience — sur le modèle de `setRootMeter()` déjà
> existant, étendu à producteur/consommateur — au lieu d'imposer une ThingClass par interface.
> Motivation : le rôle dépend de l'installation, pas du modèle ; la matrice modèle×rôle explose la
> liste d'ajout côté installateur. C'est additif → ne casse rien des setups existants.

Si nymea suit, la matrice eastron se simplifie d'elle-même (une ThingClass `energymeter` par
modèle, rôle réglé en réglages énergie). **Pas pour aujourd'hui ni le mois prochain.**

### 3.6 Config prod ETM (`etm-nymea-dev`, 192.168.1.120)

- **SDM72** = compteur **principal / réseau**, unit ID **1** → ThingClass `sdm72` (`energymeter`),
  désigné **root meter** dans les réglages énergie.
- **SDM120** = compteur **PV**, unit ID **2** → ThingClass `sdm120Producer` (`smartmeterproducer`).
  *(Un `sdm120` energymeter non-root ne serait PAS agrégé en production — il faut bien la variante
  Producer.)*
- ⚠️ Vérifier sur le Thing réel que ces variantes sont bien celles instanciées (convention ETM
  « pas de valeur inventée »), via `nymea-app` ou la conf nymead.

## 4. CARTES DE REGISTRES (confirmées depuis les `*-registers.json`)

Communes aux deux : `protocol: "RTU"`, `endianness: "BigEndian"`, `type: "float"` (32 bits, 2
registres), `registerType: "inputRegister"` (FC04), `access: "RO"`,
`errorLimitUntilNotReachable: 15`. Adresses en **base 0** (registre Modbus, pas l'offset 3xxxx).
**✅ Adresses, FC04 et word order Big-endian (MSR first) confirmés contre les protocoles
constructeur** (SDM72D-M-2 V1.1.1, SDM120 V2.4, SDM230 V1.2).

> 🔎 **`totalEnergyConsumed`/`totalEnergyProduced` = Import/Export, PAS le « Total » du compteur.**
> Le plugin lit `Import active energy` (reg 72) et `Export active energy` (reg 74), et **non** le
> registre « Total active energy » (reg 342). C'est volontaire et correct : sur SDM120/SDM230 le
> « total » dépend du réglage interne *Measurement mode* (mode 2 = import+export par défaut, mode 1
> = import seul, mode 3 = import−export) ; sur SDM72 le « total » = import+export. Lire import et
> export séparément contourne cette ambiguïté de mode. **Ne pas « simplifier » en lisant reg 342.**

### SDM72 — `className: "Sdm72"` · `checkReachableRegister: "totalCurrentPower"`
| addr | id | unité | groupé en block ? |
|---|---|---|---|
| 52 | `totalCurrentPower` (puissance système totale, 30053) | W | non (registre seul, sonde reachable) |
| 0 / 2 / 4 | `voltagePhaseA/B/C` (30001/03/05) | V | block `phaseVoltageAndCurrent` |
| 6 / 8 / 10 | `currentPhaseA/B/C` (30007/09/11) | A | block `phaseVoltageAndCurrent` |
| 12 / 14 / 16 | `powerPhaseA/B/C` (30013/15/17) | W | block `phasePower` |
| 70 | `frequency` (30071) | Hz | block `frequencyAndTotalEnergy` |
| 72 | `totalEnergyConsumed` = Import active energy (30073) | kWh | block `frequencyAndTotalEnergy` |
| 74 | `totalEnergyProduced` = Export active energy (30075) | kWh | block `frequencyAndTotalEnergy` |

### SDM120 — `className: "Sdm120"` · `checkReachableRegister: "activePower"`
| addr | id | unité | groupé en block ? |
|---|---|---|---|
| 0 | `voltage` (30001) | V | non |
| 6 | `current` (30007) | A | non |
| 12 | `activePower` (30013) | W | non (sonde reachable) |
| 70 | `frequency` (30071) | Hz | block `frequencyAndTotalEnergy` |
| 72 | `totalEnergyConsumed` = Import active energy (30073) | kWh | block `frequencyAndTotalEnergy` |
| 74 | `totalEnergyProduced` = Export active energy (30075) | kWh | block `frequencyAndTotalEnergy` |

### SDM230 — `className: "Sdm230"` (JSON non relu, **adresses confirmées par datasheet V1.2**)
Layout **identique au SDM120** (monophasé) : voltage(0), current(6), activePower(12),
frequency(70), import→`totalEnergyConsumed`(72), export→`totalEnergyProduced`(74). Le
`sdm230-registers.json` doit donc être la copie du SDM120 avec `className: "Sdm230"` — à vérifier
sur le fichier réel.

> ⚠️ **Block reads = OK ici** (contrairement au piège V2C). Les `blocks` regroupent des registres
> **contigus et non chevauchants** (le SDM expose des floats sur des adresses pairement espacées).
> Le modbus-tool lit chaque block en une transaction groupée — comportement voulu et sûr sur SDM.
> Ne PAS « casser » les blocks en lectures individuelles pour imiter V2C : c'est inutile et ça
> multiplie les transactions sur un bus déjà partagé.
>
> 📏 **Limite de transaction par modèle** (datasheets) : SDM72 = **30 paramètres / 60 registres**
> max par requête ; SDM120 et SDM230 = **40 / 80**. Les blocks actuels sont très en dessous, mais
> à respecter si on ajoute des registres à un block (un block trop large lit aussi des registres
> réservés inutiles).

## 4bis. PARAMÈTRES SÉRIE DU COMPTEUR (réglés sur le compteur, vus dans les datasheets)

Confirmé par les manuels (utile au commissioning ; rappel : ces valeurs sont côté **compteur**,
le `ModbusRtuMaster` nymea doit matcher — cf. §2) :

- **Baud supportés** : SDM72D-M-2 = 1200/2400/4800/9600/**19200** (défaut **9600**) ;
  SDM120 & SDM230 = 1200/2400/4800/9600 (défaut **2400**, **pas de 19200**).
- 🔴 **Cap bus = 9600** : un SDM120/SDM230 ne peut pas dépasser 9600. Le bus partagé est donc
  plafonné à 9600 tant qu'un de ces modèles y est. Inutile d'espérer accélérer le bus au-delà.
- 🔧 **Commissioning** : un SDM120/SDM230 sorti d'usine est à **2400** → le passer à **9600** (et
  régler son adresse Modbus unique) via le menu façade avant de le brancher au bus prod.
- **Parité/stop** : défaut **None / 1 stop**. Le bus prod est en **9600 8N1** → cohérent.
- **Word order** : MSR first (Big-endian) par défaut sur les trois ; SDM72 l'auto-détecte et ne
  l'expose pas au menu. Le plugin suppose `BigEndian` → ne pas changer ce réglage sur le compteur.

## 5. SÉMANTIQUE DE REACHABILITY (lien direct avec les timeouts)

`errorLimitUntilNotReachable: 15` + `checkReachableRegister` définissent comment le plugin gère
les erreurs Modbus :

- Le générateur lit périodiquement le `checkReachableRegister` (`totalCurrentPower` pour SDM72,
  `activePower` pour SDM120) pour juger si le device répond.
- Il faut **15 erreurs consécutives** (timeouts/exceptions) avant que le Thing bascule
  `connected = false`. En dessous, les valeurs continuent d'être servies (d'où « parfois ça
  répond, parfois rafales » sans que le Thing tombe forcément).
- 🔎 **Implication debug** : des rafales < 15 sont absorbées silencieusement côté state
  `connected`, mais polluent les logs (`SdmXXModbusRtuConnection: ... TimeoutError`) et trouent la
  mesure. Le seuil masque le symptôme, il ne le corrige pas. La cause se traite au niveau bus
  (§7), pas en montant ce seuil.

## 6. DISCOVERY

- `createMethods: ["discovery"]`, `discoveryParamTypes: [ slaveAddress (int, défaut 1) ]`.
- La discovery sonde le bus (via le `ModbusRtuMaster` courant) à l'adresse demandée et tente de
  lire le `checkReachableRegister` ; si ça répond, le device est proposé.
- ⚠️ La discovery **émet du trafic sur le bus partagé** : lancer une discovery pendant que les
  SDM prod tournent ajoute des transactions. Pas bloquant, mais à savoir pour le diagnostic.
- *(Détail d'implémentation discovery — choix du master, gestion multi-master, dédoublonnage —
  à lire dans `integrationplugineastron.cpp`, non couvert ici.)*

## 7. DISCIPLINE DE BUS RS485 — la cause racine n°1 des timeouts (OPÉRATIONNEL)

Cette section encode une leçon de prod (incident timeouts SDM apparu pendant une session de debug
v2c/Trydan avec `mbpoll` sur `/dev/ttyUSB0`). À lire avant tout diagnostic de timeout.

1. **Un seul maître.** Le RS485 est mono-maître ; nymead l'est. **Ne jamais lancer `mbpoll` /
   `modpoll` sur `/dev/ttyUSB0` pendant que nymead tourne** → deux maîtres → collisions de trames
   → timeouts intermittents par rafales. Pour sonder au `mbpoll`, **arrêter nymead d'abord**
   (`sudo systemctl stop nymead`), puis le relancer.
2. **Pas de process série résiduel.** Après une session de debug, vérifier qu'aucun `mbpoll`
   zombie ne tient le port : `pgrep -a mbpoll ; sudo lsof /dev/ttyUSB0` (doit ne montrer que
   nymead).
3. **Épingler le port par identité**, pas par `ttyUSBn` : le `ModbusRtuMaster` doit pointer un
   `/dev/serial/by-id/usb-Silicon_Labs_CP2102...-if00-port0` (cp210x), sinon renommage possible
   au reboot (ttyUSB0 ↔ ttyUSB1) si un 2e adaptateur est branché.
4. **Stabilité cp210x** : surveiller `dmesg | grep -i cp210x` (disconnect/reset) — un adaptateur
   qui décroche donne aussi des rafales, indépendamment du bus.
5. **Charge VM** : sous forte charge CPU, nymead peut ne pas tenir la cadence du master → pseudo-
   timeouts. Contributeur, rarement cause unique. **Mécanisme** (confirmé par le protocole Eastron,
   §3.3) : une trame RTU doit être un **flux continu** ; un silence > **1,5 temps-caractère**
   (≈ 1,6 ms à 9600 8N1) en milieu de trame fait **jeter** la trame par le SDM → pas de réponse →
   timeout côté master. Le *latency timer* du cp210x (défaut **16 ms**) combiné au jitter
   d'ordonnancement de la VM peut fragmenter une trame au-delà de ce seuil. Pistes : baisser le
   latency timer du cp210x (`/sys/bus/usb-serial/devices/ttyUSB0/latency_timer`), réduire la charge,
   ou prioriser nymead. À traiter seulement si le bus est propre par ailleurs (1–4 ci-dessus).
6. **Réglages bus** (au niveau master, pas plugin) : A/B non inversés, GND commun adaptateur↔SDM,
   borniers serrés, terminaison 120 Ω peu critique sur bus court 2 nœuds à 9600. Topologie en
   **daisy-chain**, pas d'étoile ni de stub (réflexions → corruption ; protocole §2.2).

## 8. CLASSES GÉNÉRÉES & LECTURE DES LOGS

Le modbus-tool génère, à partir du `className`, une classe de connexion `<ClassName>ModbusRtuConnection` :
- `Sdm72`  → `Sdm72ModbusRtuConnection`
- `Sdm120` → `Sdm120ModbusRtuConnection`

D'où les lignes de log `Sdm72ModbusRtuConnection: ModbusRtu reply error ... TimeoutError`. Un log
préfixé par une de ces classes = couche transport générée, pas la glue du plugin. Utile pour
router un bug : préfixe `SdmXXModbusRtuConnection` → registre/transport/bus ; messages du plugin
`eastron` lui-même → setup/discovery/câblage des states.

## 9. POUR ÉTENDRE (ajouter un modèle ou un registre)

Workflow déclaratif, dans l'ordre :
1. Ajouter/éditer `<modele>-registers.json` (className unique, registres `inputRegister` float
   Big-endian, choisir un `checkReachableRegister` qui répond toujours, garder les blocks
   contigus). Réutiliser les conventions SDM72/SDM120 ci-dessus.
2. Ajouter la/les ThingClass(es) dans `integrationplugineastron.json` (3 variantes de rôle si
   pertinent), avec les `paramTypes` `slaveAddress` + `modbusMasterUuid` (readOnly) et le state
   `connected` (`cached: false`).
3. **Générer de nouveaux UUID** pour chaque nouvel id (plugin/thingclass/param/state). Ne jamais
   réutiliser un UUID existant.
4. Régénérer via le modbus-tool nymea (cible du `.pro`), recompiler amd64 puis cross arm64.
5. Tester en mono-maître (nymead arrêté, `mbpoll -1 ...`) que le device répond, puis via nymead.
6. Mettre à jour `PORTING_STATUS*` / doc catalogue (badge canal/origine/stabilité) si applicable.

## 10. INTERDIT

- **Éditer les classes de connexion générées** (`SdmXXModbusRtuConnection`) à la main — elles
  sont régénérées, toute modif sera écrasée. Corriger dans les `*-registers.json`.
- **Mettre la config série (baud/parité/timeout/retries) dans le plugin** — elle appartient au
  `ModbusRtuMaster` nymea (§2).
- **Ajouter de l'écriture** (action/registre `access: WO/RW`) — un SDM est read-only.
- **Changer/réutiliser les UUID** de ThingClasses ou de plugin publiés (casse l'appairage des
  Things existants en prod).
- **Casser les blocks en lectures individuelles** pour copier le pattern V2C — non pertinent ici
  (registres contigus, pas de chevauchement firmware).
- **Lancer `mbpoll` sur le bus pendant que nymead tourne** (§7).
- **Monter `errorLimitUntilNotReachable` pour « régler » des timeouts** — masque, ne corrige pas.
- **Toucher aux autres plugins du repo** (`abbterra`, `abbb2x`, `waveshare-relay-d8`, …).

## 11. BUILD / DEPLOY

- Paquet `eastron` versionné `1.15.0+etm3`. Cross-compilation arm64 via le container
  `build-cross-arm64` (Debian trixie amd64 + `crossbuild-essential-arm64`).
- Publié sur les canaux APT `powersync-nightly` / `powersync-testing` / `powersync-stable`
  (reprepro, GPG `77033A6E…9D1B4986`). Recette complète dans `DEPLOY.md` du projet.
- Déployé sur le RPi `hems` (192.168.1.75, canal `powersync-testing`) et en prod sur
  `etm-nymea-dev` (192.168.1.120).

## 12. DEFINITION OF DONE (pour une modif de ce plugin)

1. Le plugin compile (amd64 + cross arm64) et reste cohérent avec le `.pro`.
2. Toute nouvelle valeur passe par un `*-registers.json` (déclaratif), pas par du code à la main.
3. Reachability conservée : `checkReachableRegister` valide, `errorLimitUntilNotReachable` inchangé
   sans justification.
4. UUID neufs pour tout nouvel élément ; UUID existants intacts.
5. Testé d'abord en **mono-maître** (nymead arrêté) au `mbpoll -1`, puis intégré via nymead, sur
   le bus réel.
6. États visibles et cohérents dans `nymea-app` (valeurs qui évoluent, `connected=true` stable).
7. `PORTING_STATUS*` / doc catalogue à jour si le périmètre modèle change.

## 13. À VÉRIFIER DANS LE CODE (non lu dans ce brief)

Honnêteté sur le périmètre lu : ce brief s'appuie sur les 2 cartes de registres (SDM72/SDM120) +
le JSON du plugin + les 3 protocoles constructeur (SDM72/SDM120/SDM230). Restent à confirmer en
lisant le dépôt :
- `integrationplugineastron.cpp`/`.h` : logique de discovery, sélection du `ModbusRtuMaster`,
  câblage des states, gestion de la reconnexion.
- `eastron.pro` : invocation exacte du modbus-tool, liste des `*-registers.json` compilés.
- `meta.json` : packaging (nom, deps, version).
- `sdm230-registers.json` : **adresses confirmées par le datasheet V1.2** (= SDM120) ; reste à
  vérifier que le fichier JSON existe bien et porte `className: "Sdm230"`.
- `sdm220-registers.json`, `sdm630-registers.json` : **non vérifiés** — ni le JSON ni le datasheet
  fournis (SDM630 = 3φ riche type SDM72, SDM220 = 1φ type SDM120/230). Vérifier adresses,
  `checkReachableRegister` et blocks avant de s'y fier.
- Variante réellement instanciée pour le SDM72 (principal) et le SDM120 (PV) sur `etm-nymea-dev`
  (cf. §3) — à lire sur le Thing réel, pas à supposer.

## 14. RÉFÉRENCES

- Source plugin : `git.etm-powersync.fr/ETM-Schurig/etm-powersync-plugins-modbus`, dossier
  `eastron/` (commit `b1da668`).
- Cartes registres : `eastron/sdm72-registers.json`, `eastron/sdm120-registers.json` (+ 630/220/230).
- ThingClasses/interfaces/states : `eastron/integrationplugineastron.json`.
- Modèle de structure dans le repo : `abbterra` (evcharger Modbus TCP), `eastron` lui-même
  (pattern registres RTU déclaratif) — cités comme références dans le brief V2C.
- Datasheets/protocoles constructeur : Eastron SDM72D-M-2 User Manual V1.1.1 (2023, meter code
  `00 89`), SDM120-Modbus Protocol V2.4, SDM230-Modbus User Manual + Protocol V1.2. Registres
  input float IEEE754 Big-endian (MSR first), FC04 ; limites de transaction 30 (SDM72) / 40
  (SDM120/230) paramètres ; baud max 9600 (SDM120/230) vs 19200 (SDM72).
