diff --git a/.gitea/workflows/docs.yml b/.gitea/workflows/docs.yml
new file mode 100644
index 0000000..cf7fdd1
--- /dev/null
+++ b/.gitea/workflows/docs.yml
@@ -0,0 +1,94 @@
+name: Build & Deploy docs
+
+on:
+ push:
+ branches: [main]
+ schedule:
+ - cron: '0 3 * * *' # mise à jour nocturne (meta.json des repos plugins)
+ workflow_dispatch:
+
+jobs:
+ build-deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install dependencies
+ run: pip install -r requirements.txt
+
+ # ── Récupération des JSON depuis les 5 repos drivers ─────────────────
+ - name: Fetch plugin JSON files
+ env:
+ GITEA_TOKEN: ${{ secrets.MKDOCS_TOKEN }}
+ run: |
+ GITEA_BASE="https://git.etm-powersync.fr"
+ AUTH_BASE="https://pakutz79:${GITEA_TOKEN}@git.etm-powersync.fr"
+ mkdir -p .plugins-src
+
+ for repo in etm-powersync-plugins etm-powersync-plugins-modbus \
+ nymea-plugins nymea-plugins-modbus nymea-generic; do
+
+ # Branche par défaut via API Gitea (pas de hardcoding main/master)
+ BRANCH=$(curl -sf \
+ -H "Authorization: token ${GITEA_TOKEN}" \
+ "${GITEA_BASE}/api/v1/repos/ETM-Schurig/${repo}" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('default_branch','main'))" \
+ 2>/dev/null) || BRANCH="main"
+
+ echo "→ ${repo} (branche: ${BRANCH})"
+ git clone --depth 1 --branch "${BRANCH}" \
+ "${AUTH_BASE}/ETM-Schurig/${repo}.git" \
+ ".plugins-src/${repo}" \
+ || echo "WARNING: ${repo} introuvable ou inaccessible — ignoré"
+ done
+
+ # ── Génération de la doc ──────────────────────────────────────────────
+ - name: Generate device reference + SUMMARY.md
+ run: |
+ python3 scripts/gen_device_reference.py \
+ --src .plugins-src \
+ --docs docs \
+ --lang fr
+
+ # ── Build MkDocs ──────────────────────────────────────────────────────
+ - name: MkDocs build --strict
+ run: mkdocs build --strict
+
+ # ── Vérification idempotence ──────────────────────────────────────────
+ - name: Check generated content is up-to-date
+ run: |
+ python3 scripts/gen_device_reference.py \
+ --src .plugins-src \
+ --docs docs \
+ --lang fr \
+ --check
+
+ # ── SSH ───────────────────────────────────────────────────────────────
+ - name: Setup SSH deploy key
+ env:
+ SSH_KEY: ${{ secrets.DOCS_DEPLOY_SSH_KEY }}
+ DEPLOY_HOST: ${{ secrets.DOCS_DEPLOY_HOST }}
+ run: |
+ mkdir -p ~/.ssh
+ printf '%s\n' "${SSH_KEY}" > ~/.ssh/deploy_key
+ chmod 600 ~/.ssh/deploy_key
+ ssh-keyscan -H "${DEPLOY_HOST}" >> ~/.ssh/known_hosts
+
+ # ── Déploiement ───────────────────────────────────────────────────────
+ - name: Deploy via rsync
+ env:
+ DEPLOY_USER: ${{ secrets.DOCS_DEPLOY_USER }}
+ DEPLOY_HOST: ${{ secrets.DOCS_DEPLOY_HOST }}
+ DEPLOY_PATH: ${{ secrets.DOCS_DEPLOY_PATH }}
+ run: |
+ rsync -az --delete \
+ -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes" \
+ site/ \
+ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}"
diff --git a/.gitignore b/.gitignore
index 21d0b89..9b3a049 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
.venv/
+site/
+.plugins-src/
+.claude/
diff --git a/PORTING_STATUS.yaml b/PORTING_STATUS.yaml
new file mode 100644
index 0000000..733a655
--- /dev/null
+++ b/PORTING_STATUS.yaml
@@ -0,0 +1,63 @@
+# Source de vérité : canal APT + placement nav.
+# title/tagline/stability/icon → meta.json du repo plugin.
+# slug : nom de fichier de la fiche (défaut = plugin).
+
+- repo: etm-powersync-plugins-modbus
+ plugin: eastron
+ name: Eastron SDM
+ channel: stable
+ category: compteur
+
+- repo: etm-powersync-plugins-modbus
+ plugin: abbb2x
+ slug: abb-b2x
+ name: Compteur ABB B2x
+ channel: testing
+ category: compteur
+
+- repo: etm-powersync-plugins-modbus
+ plugin: waveshare-relay-d8
+ slug: waveshare
+ name: Waveshare relais
+ channel: testing
+ category: smartdevice
+
+- repo: etm-powersync-plugins-modbus
+ plugin: abbterra
+ slug: abb-terra
+ name: Borne ABB Terra AC
+ channel: testing
+ category: irve
+
+- repo: nymea-plugins
+ plugin: keba
+ name: Keba
+ channel: nightly
+ category: irve
+
+- repo: nymea-plugins
+ plugin: fronius
+ name: Fronius
+ channel: nightly
+ category: onduleur
+
+- repo: nymea-plugins
+ plugin: daikinairco
+ name: Daikin
+ channel: nightly
+ category: hvac
+ subcategory: climatisation
+
+- repo: nymea-plugins
+ plugin: sgready
+ name: SG-Ready
+ channel: nightly
+ category: hvac
+ subcategory: pac
+
+- repo: nymea-plugins
+ plugin: simpleheatpump
+ name: SimpleHeatpump
+ channel: nightly
+ category: hvac
+ subcategory: pac
diff --git a/docs/appareils/SUMMARY.md b/docs/appareils/SUMMARY.md
new file mode 100644
index 0000000..19310ee
--- /dev/null
+++ b/docs/appareils/SUMMARY.md
@@ -0,0 +1,15 @@
+* [Compatibilité](compatibilite.md)
+* [Compteurs](compteurs/index.md)
+ * [Eastron SDM](compteurs/eastron.md)
+ * [Compteur ABB B2x](compteurs/abb-b2x.md)
+* [Bornes de recharge](bornes/index.md)
+ * [Borne ABB Terra AC](bornes/abb-terra.md)
+ * [Keba](bornes/keba.md)
+* [SmartDevices](smart/index.md)
+ * [Waveshare relais](smart/waveshare.md)
+* [HVAC](hvac/index.md)
+ * [Daikin](hvac/daikinairco.md)
+ * [SG-Ready](hvac/sgready.md)
+ * [SimpleHeatpump](hvac/simpleheatpump.md)
+* [Onduleurs / PV](onduleurs/index.md)
+ * [Fronius](onduleurs/fronius.md)
diff --git a/docs/appareils/bornes.md b/docs/appareils/bornes.md
deleted file mode 100644
index 07e278d..0000000
--- a/docs/appareils/bornes.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Bornes de recharge
-
-> Stub — détail des bornes supportées, registres Modbus, particularités d'intégration.
diff --git a/docs/appareils/bornes/abb-terra.md b/docs/appareils/bornes/abb-terra.md
new file mode 100644
index 0000000..cfdb419
--- /dev/null
+++ b/docs/appareils/bornes/abb-terra.md
@@ -0,0 +1,104 @@
+# Borne ABB Terra AC
+
+TESTING CONSUMER
+
+La borne de recharge ABB Terra AC existe en deux variantes de communication :
+**Modbus TCP** (réseau Ethernet/LAN) et **Modbus RTU** (bus RS485).
+
+## 1. Choix de la variante
+
+- **TCP** : la borne est sur le réseau local, repérée par son adresse IP /
+ nom d'hôte. Ajout par découverte réseau.
+- **RTU** : la borne est sur un bus RS485, repérée par son adresse esclave.
+
+## 2. Raccordement
+
+- TCP : câble Ethernet vers le réseau du hub.
+- RTU : A↔A, B↔B, masse, terminaison 120 Ω.
+
+## 3. Ajout dans PowerSync
+
+Variante TCP : **découverte automatique** sur le réseau, ou ajout **manuel**
+(IP, port). Variante RTU : ajout via le maître RTU + adresse esclave.
+Voir [Ajouter un appareil](../../installation/application.md).
+
+## 4. Vérification
+
+`connected`, `pluggedIn` puis `charging` reflètent l'état de la session ;
+`maxChargingCurrent` reflète la consigne de courant.
+
+---
+
+## Référence
+
+
+**Fabricant :** ABB
+**Plugin :** `AbbTerra`
+
+#### Modèles pris en charge
+| Modèle | Rôle | Transport | Ajout | Grandeurs |
+| --- | --- | --- | --- | --- |
+| **Terra AC Charger (TCP)** | Borne de recharge | Modbus TCP | Découverte automatique / Ajout manuel | 17 |
+| **Terra AC Charger (RTU)** | Borne de recharge | Modbus RTU | Découverte automatique / Ajout manuel | 17 |
+
+#### Détail par modèle
+??? abstract "Terra AC Charger (TCP) — `terraAcTcp`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `macAddress` | MAC address | QString | — | — | oui |
+ | `address` | Host address | QString | — | — | non |
+ | `hostName` | Host name | QString | — | — | non |
+ | `port` | Port | uint | — | `502` | non |
+ | `slaveId` | Slave ID | uint | 1–255 | `1` | non |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `pluggedIn` | Plugged in | bool | — |
+ | `charging` | Charging | bool | — |
+ | `power` | Charging enabled | bool | — |
+ | `maxChargingCurrent` | Maximum charging current | double | Ampere |
+ | `phaseCount` | Phase count | uint | — |
+ | `currentPower` | Active power | double | Watt |
+ | `currentPhase1` | Current phase 1 | double | Ampere |
+ | `currentPhase2` | Current phase 2 | double | Ampere |
+ | `currentPhase3` | Current phase 3 | double | Ampere |
+ | `voltagePhase1` | Voltage phase 1 | double | Volt |
+ | `voltagePhase2` | Voltage phase 2 | double | Volt |
+ | `voltagePhase3` | Voltage phase 3 | double | Volt |
+ | `sessionEnergy` | Session energy | double | KiloWattHour |
+ | `firmwareVersion` | Firmware version | QString | — |
+ | `serialNumber` | Serial number | QString | — |
+ | `errorCode` | Error code | uint | — |
+
+??? abstract "Terra AC Charger (RTU) — `terraAcRtu`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `rtuMaster` | Modbus RTU master | QString | — | — | non |
+ | `slaveId` | Modbus slave ID | uint | 1–247 | `1` | non |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `pluggedIn` | Plugged in | bool | — |
+ | `charging` | Charging | bool | — |
+ | `power` | Charging enabled | bool | — |
+ | `maxChargingCurrent` | Maximum charging current | double | Ampere |
+ | `phaseCount` | Phase count | uint | — |
+ | `currentPower` | Active power | double | Watt |
+ | `currentPhase1` | Current phase 1 | double | Ampere |
+ | `currentPhase2` | Current phase 2 | double | Ampere |
+ | `currentPhase3` | Current phase 3 | double | Ampere |
+ | `voltagePhase1` | Voltage phase 1 | double | Volt |
+ | `voltagePhase2` | Voltage phase 2 | double | Volt |
+ | `voltagePhase3` | Voltage phase 3 | double | Volt |
+ | `sessionEnergy` | Session energy | double | KiloWattHour |
+ | `firmwareVersion` | Firmware version | QString | — |
+ | `serialNumber` | Serial number | QString | — |
+ | `errorCode` | Error code | uint | — |
+
+
diff --git a/docs/appareils/bornes/index.md b/docs/appareils/bornes/index.md
new file mode 100644
index 0000000..ffaebfd
--- /dev/null
+++ b/docs/appareils/bornes/index.md
@@ -0,0 +1,8 @@
+# Bornes de recharge
+
+Bornes EVSE intégrées dans ETM PowerSync pour la recharge pilotée des véhicules électriques.
+
+| Appareil | Protocole | Canal | Stabilité |
+|---|---|---|---|
+| [Borne ABB Terra AC](abb-terra.md) | Modbus TCP / RTU | TESTING | CONSUMER |
+| [Keba](keba.md) | Modbus TCP | NIGHTLY | |
diff --git a/docs/appareils/bornes/keba.md b/docs/appareils/bornes/keba.md
new file mode 100644
index 0000000..e41666f
--- /dev/null
+++ b/docs/appareils/bornes/keba.md
@@ -0,0 +1,61 @@
+# Keba
+
+NIGHTLY
+
+Bornes de recharge Keba (séries P30, P31), communication **Modbus TCP** via réseau local.
+
+## 1. Matériel requis
+
+- Borne Keba P30 ou P31
+- Connexion Ethernet vers le réseau du hub
+
+## 2. Activation Modbus TCP
+
+Modbus TCP est **désactivé par défaut** sur les bornes Keba.
+Activez-le dans l'interface web de la borne (port `502`).
+
+!!! warning "Activation Modbus"
+ Sans cette étape, la borne ne répondra pas à PowerSync.
+
+## 3. Ajout dans PowerSync
+
+Ajout par **découverte automatique** sur le réseau, ou **manuel** (IP, port 502).
+Voir [Ajouter un appareil](../../installation/application.md).
+
+## 4. Vérification
+
+`connected`, `pluggedIn` puis `charging` reflètent l'état de la session.
+
+---
+
+## Référence
+
+
+**Fabricant :** Keba
+**Plugin :** `keba`
+
+#### Modèles pris en charge
+| Modèle | Rôle | Transport | Ajout | Grandeurs |
+| --- | --- | --- | --- | --- |
+| **Keba P30 / P31** | Borne de recharge | Modbus TCP | Découverte automatique / Ajout manuel | 6 |
+
+#### Détail par modèle
+??? abstract "Keba P30 / P31 — `kebaEVCharger`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `macAddress` | MAC address | QString | — | — | oui |
+ | `address` | Host address | QString | — | — | non |
+ | `port` | Port | uint | — | `502` | non |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `pluggedIn` | Plugged in | bool | — |
+ | `charging` | Charging | bool | — |
+ | `maxChargingCurrent` | Maximum charging current | double | Ampere |
+ | `currentPower` | Active power | double | Watt |
+ | `sessionEnergy` | Session energy | double | KiloWattHour |
+
+
diff --git a/docs/appareils/compatibilite.md b/docs/appareils/compatibilite.md
index f06f5ac..75eed93 100644
--- a/docs/appareils/compatibilite.md
+++ b/docs/appareils/compatibilite.md
@@ -1,39 +1,47 @@
# Compatibilité
-Liste de référence des équipements. **Source de vérité** : générée à partir du suivi de
-portage (`PORTING_STATUS`). Statuts : [Supporté]{.badge .ok} ·
-[Partiel]{.badge .part} · [Roadmap]{.badge .road}.
+Liste de référence des équipements supportés par ETM PowerSync. Générée depuis
+`PORTING_STATUS.yaml` — ne pas modifier manuellement entre les marqueurs.
-!!! warning "À automatiser"
- Cette page sera générée depuis `PORTING_STATUS` pour éviter la double saisie.
- Le tableau ci-dessous est un point de départ manuel.
-
-## Bornes de recharge (EVSE)
-
-| Marque / Modèle | Protocole | Statut |
-|---|---|---|
-| Keba | Modbus TCP | [Partiel]{.badge .part} |
+| Badge | Signification |
+|---|---|
+| STABLE | Disponible sur le dépôt APT **stable** — prêt pour la production |
+| TESTING | Disponible sur le dépôt APT **testing** — fonctionnel, en cours d'épreuve |
+| NIGHTLY | Portage en cours — non disponible en production |
+
## Compteurs
-| Marque / Modèle | Protocole | Statut |
-|---|---|---|
-| Eastron SDM | Modbus RTU | [Supporté]{.badge .ok} |
-| Waveshare (relais) | Modbus | [Supporté]{.badge .ok} |
+| Marque / Modèle | Protocole | Canal | Stabilité |
+|---|---|---|---|
+| Eastron SDM | Modbus RTU | STABLE | CONSUMER |
+| Compteur ABB B2x | Modbus RTU | TESTING | CONSUMER |
+
+## Bornes de recharge
+
+| Marque / Modèle | Protocole | Canal | Stabilité |
+|---|---|---|---|
+| Borne ABB Terra AC | Modbus TCP | TESTING | CONSUMER |
+| Keba | Modbus TCP | NIGHTLY | CONSUMER |
+
+## SmartDevices
+
+| Marque / Modèle | Protocole | Canal | Stabilité |
+|---|---|---|---|
+| Waveshare relais | Modbus RTU | TESTING | CONSUMER |
+
+## HVAC
+
+| Marque / Modèle | Protocole | Canal | Stabilité |
+|---|---|---|---|
+| Daikin | — | NIGHTLY | CONSUMER |
+| SG-Ready | — | NIGHTLY | CONSUMER |
+| SimpleHeatpump | — | NIGHTLY | CONSUMER |
## Onduleurs / PV
-| Marque / Modèle | Protocole | Statut |
-|---|---|---|
-| SMA | Modbus / SunSpec | [Partiel]{.badge .part} |
-| Fronius | Modbus / SunSpec | [Roadmap]{.badge .road} |
-| Huawei | Modbus | [Roadmap]{.badge .road} |
-| SolarEdge | Modbus | [Roadmap]{.badge .road} |
+| Marque / Modèle | Protocole | Canal | Stabilité |
+|---|---|---|---|
+| Fronius | Modbus TCP | NIGHTLY | CONSUMER |
-## Batteries / ESS
-
-| Marque / Modèle | Protocole | Statut |
-|---|---|---|
-| Victron | Modbus / MQTT | [Roadmap]{.badge .road} |
-
-> Stub — compléter à partir du suivi de portage réel.
+
diff --git a/docs/appareils/compteurs.md b/docs/appareils/compteurs.md
deleted file mode 100644
index 00d5f75..0000000
--- a/docs/appareils/compteurs.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Compteurs
-
-> Stub — compteurs d'énergie supportés (Eastron SDM, etc.), câblage, configuration Modbus.
diff --git a/docs/appareils/compteurs/abb-b2x.md b/docs/appareils/compteurs/abb-b2x.md
new file mode 100644
index 0000000..5382378
--- /dev/null
+++ b/docs/appareils/compteurs/abb-b2x.md
@@ -0,0 +1,71 @@
+# Compteur ABB B2x
+
+TESTING CONSUMER
+
+Le compteur d'énergie ABB B2x communique en **Modbus RTU** sur le bus RS485 du
+hub. Mesure triphasée (tensions, courants et puissances par phase).
+
+## 1. Matériel requis
+
+- Compteur ABB B2x
+- Adaptateur USB↔RS485 côté hub
+- Câble bus 2 fils (A/B) + masse
+
+## 2. Raccordement RS485
+
+A↔A, B↔B, masse commune, terminaison **120 Ω** en bout de bus.
+
+## 3. Adressage Modbus
+
+Adresse esclave unique sur le bus (`1`–`254`, défaut `1`).
+
+## 4. Ajout dans PowerSync
+
+Ajout par **découverte automatique** ou **manuel** (saisie de l'adresse esclave).
+Voir [Ajouter un appareil](../../installation/application.md).
+
+---
+
+## Référence
+
+
+**Fabricant :** ABB
+**Plugin :** `AbbB2x`
+
+#### Modèles pris en charge
+| Modèle | Rôle | Transport | Ajout | Grandeurs |
+| --- | --- | --- | --- | --- |
+| **ABB B2x energy meter** | Compteur d'énergie | Modbus RTU | Découverte automatique / Ajout manuel | 14 |
+
+#### Détail par modèle
+??? abstract "ABB B2x energy meter — `abbB2x`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | 1–254 | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | 1–254 | `1` | oui |
+ | `modbusMasterUuid` | Modbus RTU master | QString | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+ | `voltagePhaseA` | Voltage phase A | double | Volt |
+ | `voltagePhaseB` | Voltage phase B | double | Volt |
+ | `voltagePhaseC` | Voltage phase C | double | Volt |
+ | `currentPhaseA` | Current phase A | double | Ampere |
+ | `currentPhaseB` | Current phase B | double | Ampere |
+ | `currentPhaseC` | Current phase C | double | Ampere |
+ | `currentPowerPhaseA` | Current power phase A | double | Watt |
+ | `currentPowerPhaseB` | Current power phase B | double | Watt |
+ | `currentPowerPhaseC` | Current power phase C | double | Watt |
+
+
diff --git a/docs/appareils/compteurs/eastron.md b/docs/appareils/compteurs/eastron.md
new file mode 100644
index 0000000..c74466e
--- /dev/null
+++ b/docs/appareils/compteurs/eastron.md
@@ -0,0 +1,407 @@
+# Compteurs Eastron (SDM)
+
+STABLE CONSUMER
+
+Les compteurs Eastron de la série SDM (SDM72, SDM120, SDM220, SDM230, SDM630)
+communiquent en **Modbus RTU** sur le bus RS485 du hub. Selon le modèle, ils
+mesurent un raccordement monophasé ou triphasé et peuvent être affectés à trois
+rôles : compteur général, compteur de consommation ou compteur de production.
+
+## 1. Matériel requis
+
+- Le compteur Eastron (modèle selon le besoin de mesure)
+- Un adaptateur USB↔RS485 côté hub
+- Câble bus 2 fils torsadés (A/B) + masse
+
+## 2. Raccordement RS485
+
+Relier A↔A, B↔B entre l'adaptateur et le compteur, masse commune. Placer une
+résistance de terminaison **120 Ω** à chaque extrémité du bus.
+
+!!! warning "À valider sur votre banc"
+ Vitesse (baudrate) et parité par défaut du compteur : à confirmer dans le
+ menu de l'appareil avant mise en service.
+
+## 3. Adressage Modbus
+
+Chaque appareil du bus doit avoir une **adresse esclave unique** (réglée via le
+menu du compteur). L'adresse par défaut est `1`. Si plusieurs Eastron partagent
+le bus, attribuez `1`, `2`, `3`, …
+
+## 4. Ajout et configuration dans l'application
+
+L'ajout se fait depuis l'application, en mode installateur — voir
+[Ajouter un appareil](../../installation/application.md). Pour l'Eastron :
+découverte sur le bus, sélection du modèle, puis choix du **rôle** (compteur
+général / consommation / production).
+
+## 5. Vérification
+
+Une fois ajouté, l'état `connected` passe à vrai et les grandeurs (`currentPower`,
+etc.) se mettent à jour. En cas d'absence de données : vérifier câblage A/B,
+adresse esclave et terminaison.
+
+---
+
+## Référence {#reference}
+
+
+**Fabricant :** Eastron
+**Plugin :** `eastron`
+
+#### Modèles pris en charge
+| Modèle | Rôle | Transport | Ajout | Grandeurs |
+| --- | --- | --- | --- | --- |
+| **SDM630 — Energy Meter** | Compteur d'énergie | Modbus RTU | Découverte automatique | 20 |
+| **SDM630 — Consumer Meter** | Compteur de consommation | Modbus RTU | Découverte automatique | 4 |
+| **SDM630 — Producer Meter** | Compteur de production | Modbus RTU | Découverte automatique | 4 |
+| **SDM72 — Energy Meter** | Compteur d'énergie | Modbus RTU | Découverte automatique | 14 |
+| **SDM72 — Consumer Meter** | Compteur de consommation | Modbus RTU | Découverte automatique | 4 |
+| **SDM72 — Producer Meter** | Compteur de production | Modbus RTU | Découverte automatique | 4 |
+| **SDM120 — Energy Meter** | Compteur d'énergie | Modbus RTU | Découverte automatique | 7 |
+| **SDM120 — Consumer Meter** | Compteur de consommation | Modbus RTU | Découverte automatique | 4 |
+| **SDM120 — Producer Meter** | Compteur de production | Modbus RTU | Découverte automatique | 4 |
+| **SDM220 — Energy Meter** | Compteur d'énergie | Modbus RTU | Découverte automatique | 7 |
+| **SDM220 — Consumer Meter** | Compteur de consommation | Modbus RTU | Découverte automatique | 4 |
+| **SDM220 — Producer Meter** | Compteur de production | Modbus RTU | Découverte automatique | 4 |
+| **SDM230 — Energy Meter** | Compteur d'énergie | Modbus RTU | Découverte automatique | 7 |
+| **SDM230 — Consumer Meter** | Compteur de consommation | Modbus RTU | Découverte automatique | 4 |
+| **SDM230 — Producer Meter** | Compteur de production | Modbus RTU | Découverte automatique | 4 |
+
+#### Détail par modèle
+??? abstract "SDM630 — Energy Meter — `sdm630`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `voltagePhaseA` | Voltage phase A | double | Volt |
+ | `voltagePhaseB` | Voltage phase B | double | Volt |
+ | `voltagePhaseC` | Voltage phase C | double | Volt |
+ | `currentPhaseA` | Current phase A | double | Ampere |
+ | `currentPhaseB` | Current phase B | double | Ampere |
+ | `currentPhaseC` | Current phase C | double | Ampere |
+ | `currentPower` | Current power | double | Watt |
+ | `currentPowerPhaseA` | Current power phase A | double | Watt |
+ | `currentPowerPhaseB` | Current power phase B | double | Watt |
+ | `currentPowerPhaseC` | Current power phase C | double | Watt |
+ | `frequency` | Frequency | double | Hertz |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+ | `energyConsumedPhaseA` | Energy consumed phase A | double | KiloWattHour |
+ | `energyConsumedPhaseB` | Energy consumed phase B | double | KiloWattHour |
+ | `energyConsumedPhaseC` | Energy consumed phase C | double | KiloWattHour |
+ | `energyProducedPhaseA` | Energy produced phase A | double | KiloWattHour |
+ | `energyProducedPhaseB` | Energy produced phase B | double | KiloWattHour |
+ | `energyProducedPhaseC` | Energy produced phase C | double | KiloWattHour |
+
+??? abstract "SDM630 — Consumer Meter — `sdm630Consumer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "SDM630 — Producer Meter — `sdm630Producer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "SDM72 — Energy Meter — `sdm72`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `voltagePhaseA` | Voltage phase A | double | Volt |
+ | `voltagePhaseB` | Voltage phase B | double | Volt |
+ | `voltagePhaseC` | Voltage phase C | double | Volt |
+ | `currentPhaseA` | Current phase A | double | Ampere |
+ | `currentPhaseB` | Current phase B | double | Ampere |
+ | `currentPhaseC` | Current phase C | double | Ampere |
+ | `currentPower` | Current power | double | Watt |
+ | `currentPowerPhaseA` | Current power phase A | double | Watt |
+ | `currentPowerPhaseB` | Current power phase B | double | Watt |
+ | `currentPowerPhaseC` | Current power phase C | double | Watt |
+ | `frequency` | Frequency | double | Hertz |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+
+??? abstract "SDM72 — Consumer Meter — `sdm72Consumer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "SDM72 — Producer Meter — `sdm72Producer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "SDM120 — Energy Meter — `sdm120`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `voltagePhaseA` | Voltage | double | Volt |
+ | `currentPhaseA` | Current | double | Ampere |
+ | `currentPower` | Current power | double | Watt |
+ | `frequency` | Frequency | double | Hertz |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+
+??? abstract "SDM120 — Consumer Meter — `sdm120Consumer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "SDM120 — Producer Meter — `sdm120Producer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "SDM220 — Energy Meter — `sdm220`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `voltagePhaseA` | Voltage | double | Volt |
+ | `currentPhaseA` | Current | double | Ampere |
+ | `currentPower` | Current power | double | Watt |
+ | `frequency` | Frequency | double | Hertz |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+
+??? abstract "SDM220 — Consumer Meter — `sdm220Consumer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "SDM220 — Producer Meter — `sdm220Producer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "SDM230 — Energy Meter — `sdm230`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `voltagePhaseA` | Voltage | double | Volt |
+ | `currentPhaseA` | Current | double | Ampere |
+ | `currentPower` | Current power | double | Watt |
+ | `frequency` | Frequency | double | Hertz |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+
+??? abstract "SDM230 — Consumer Meter — `sdm230Consumer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyConsumed` | Total energy consumed | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "SDM230 — Producer Meter — `sdm230Producer`"
+ _Paramètres de découverte :_
+ | Clé | Libellé | Type | Plage | Défaut |
+ | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Slave address | int | — | `1` |
+
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `slaveAddress` | Modbus slave address | uint | — | `1` | non |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `totalEnergyProduced` | Total energy produced | double | KiloWattHour |
+ | `frequency` | Frequency | double | Hertz |
+
+
diff --git a/docs/appareils/compteurs/index.md b/docs/appareils/compteurs/index.md
new file mode 100644
index 0000000..26b714a
--- /dev/null
+++ b/docs/appareils/compteurs/index.md
@@ -0,0 +1,8 @@
+# Compteurs
+
+Compteurs d'énergie supportés par ETM PowerSync via Modbus RTU.
+
+| Appareil | Protocole | Canal | Stabilité |
+|---|---|---|---|
+| [Eastron SDM](eastron.md) | Modbus RTU | STABLE | CONSUMER |
+| [Compteur ABB B2x](abb-b2x.md) | Modbus RTU | TESTING | CONSUMER |
diff --git a/docs/appareils/index.md b/docs/appareils/index.md
index 405bb99..4c7154f 100644
--- a/docs/appareils/index.md
+++ b/docs/appareils/index.md
@@ -1,10 +1,9 @@
# Appareils
-ETM PowerSync communique avec les équipements via Modbus (TCP/RTU), MQTT et protocoles
-propriétaires, grâce aux plugins du dépôt
-[powersync-plugins](https://github.com/etmschurig/powersync-plugins).
+ETM PowerSync communique avec les équipements via Modbus (TCP/RTU) et protocoles
+propriétaires, grâce aux plugins embarqués.
-- [Compatibilité](compatibilite.md) — liste de référence filtrable
-- [Bornes de recharge](bornes.md)
-- [Compteurs](compteurs.md)
-- [Onduleurs / PV](onduleurs.md)
+- [Compatibilité](compatibilite.md) — matrice complète avec canaux APT
+- [Compteurs](compteurs/index.md) — Eastron SDM, ABB B2x
+- [Bornes de recharge](bornes/index.md) — ABB Terra AC, Keba
+- [SmartDevices](smart/index.md) — Waveshare
diff --git a/docs/appareils/onduleurs.md b/docs/appareils/onduleurs.md
deleted file mode 100644
index e8d053b..0000000
--- a/docs/appareils/onduleurs.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Onduleurs / PV
-
-> Stub — onduleurs et données PV (SunSpec/Modbus), lecture de production.
diff --git a/docs/appareils/smart/index.md b/docs/appareils/smart/index.md
new file mode 100644
index 0000000..f81cdee
--- /dev/null
+++ b/docs/appareils/smart/index.md
@@ -0,0 +1,7 @@
+# SmartDevices
+
+Modules de pilotage de charges supportés par ETM PowerSync.
+
+| Appareil | Protocole | Canal | Stabilité |
+|---|---|---|---|
+| [Waveshare relais](waveshare.md) | Modbus RTU | TESTING | CONSUMER |
diff --git a/docs/appareils/smart/waveshare.md b/docs/appareils/smart/waveshare.md
new file mode 100644
index 0000000..38f8b83
--- /dev/null
+++ b/docs/appareils/smart/waveshare.md
@@ -0,0 +1,64 @@
+# Waveshare relais
+
+TESTING CONSUMER
+
+Module de relais Waveshare (modèle 8 canaux RS485), pilotage via **Modbus RTU**.
+
+## 1. Matériel requis
+
+- Module Waveshare 8-Channel Relay (RS485)
+- Adaptateur USB↔RS485 côté hub
+- Alimentation 12 V DC + câble bus (A/B)
+
+## 2. Raccordement RS485
+
+A↔A, B↔B, masse commune, terminaison **120 Ω** en bout de bus.
+Alimentation 12 V DC sur les bornes VCC/GND du module.
+
+## 3. Adressage Modbus
+
+Adresse par défaut : **1**. Configurable via l'utilitaire Waveshare ou par
+commande Modbus de changement d'adresse.
+
+Paramètres : **9600 bps, 8N1**.
+
+## 4. Ajout dans PowerSync
+
+Ajout manuel (port série, adresse esclave).
+Voir [Ajouter un appareil](../../installation/application.md).
+
+---
+
+## Référence
+
+
+**Fabricant :** Waveshare
+**Plugin :** `waveshare-relay-d8`
+
+#### Modèles pris en charge
+| Modèle | Rôle | Transport | Ajout | Grandeurs |
+| --- | --- | --- | --- | --- |
+| **Waveshare 8-Channel Relay (RS485)** | — | Modbus RTU | Ajout manuel | 9 |
+
+#### Détail par modèle
+??? abstract "Waveshare 8-Channel Relay (RS485) — `waveshareRelayD8`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `modbusMasterUuid` | Modbus RTU master | QUuid | — | — | oui |
+ | `slaveAddress` | Slave address | uint | 1–247 | `1` | non |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `relay1` | Relay 1 | bool | — |
+ | `relay2` | Relay 2 | bool | — |
+ | `relay3` | Relay 3 | bool | — |
+ | `relay4` | Relay 4 | bool | — |
+ | `relay5` | Relay 5 | bool | — |
+ | `relay6` | Relay 6 | bool | — |
+ | `relay7` | Relay 7 | bool | — |
+ | `relay8` | Relay 8 | bool | — |
+
+
diff --git a/docs/fonctionnalites/api.md b/docs/fonctionnalites/api.md
deleted file mode 100644
index 1c08c7f..0000000
--- a/docs/fonctionnalites/api.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# API REST / MQTT [Supporté]{.badge .ok}
-
-ETM PowerSync expose l'API JSON-RPC de nymea, permettant l'intégration domotique
-(Home Assistant, etc.). Vos données restent accessibles, en local.
-
-Voir [Intégrations](../integrations/rest-api.md) pour les détails.
-
-> Stub — documenter les endpoints principaux.
diff --git a/docs/fonctionnalites/index.md b/docs/fonctionnalites/index.md
index 04284c4..d0f515f 100644
--- a/docs/fonctionnalites/index.md
+++ b/docs/fonctionnalites/index.md
@@ -4,14 +4,14 @@ Vue d'ensemble des fonctions du HEMS et de leur état réel.
| Fonction | État |
|---|---|
-| [Surplus solaire](surplus-solaire.md) | [Partiel]{.badge .part} |
-| [Délestage / Load management](delestage.md) | [Partiel]{.badge .part} |
-| [Gestion batterie](gestion-batterie.md) | [Partiel]{.badge .part} |
-| [API REST / MQTT](api.md) | [Supporté]{.badge .ok} |
-| Tarifs dynamiques (FR) | [Roadmap]{.badge .road} |
-| Optimisation CO₂ (RTE) | [Roadmap]{.badge .road} |
-| Planificateur de charge | [Roadmap]{.badge .road} |
-| Pompe à chaleur | [Roadmap]{.badge .road} |
+| [Surplus solaire](surplus-solaire.md) | EN COURS |
+| [Délestage / Load management](delestage.md) | EN COURS |
+| [Gestion batterie](gestion-batterie.md) | EN COURS |
+| [API REST](../integrations/rest-api.md) / [MQTT](../integrations/mqtt.md) | DISPONIBLE |
+| Tarifs dynamiques (FR) | ROADMAP |
+| Optimisation CO₂ (RTE) | ROADMAP |
+| Planificateur de charge | ROADMAP |
+| Pompe à chaleur | ROADMAP |
!!! note
Les fonctions en *Roadmap* ne sont pas encore disponibles. Elles sont listées par
diff --git a/docs/index.md b/docs/index.md
index b3a7550..86061d9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -8,9 +8,9 @@ Bâti sur [nymea.io](https://nymea.io), selon une architecture **Open Core** : u
libre (GPL-3.0) et une couche d'optimisation propriétaire.
!!! info "Statuts honnêtes"
- Chaque fonction affiche son état réel : [:material-circle:{.ok} **Supporté**]{.badge .ok}
- en production · [:material-circle:{.part} **Partiel**]{.badge .part} en industrialisation ·
- [:material-circle:{.road} **Roadmap**]{.badge .road} planifié. On ne documente pas une promesse.
+ Chaque fonction affiche son état réel : STABLE
+ en production · EN COURS en industrialisation ·
+ ROADMAP planifié. On ne documente pas une promesse.
## Par où commencer
diff --git a/docs/installation/application.md b/docs/installation/application.md
new file mode 100644
index 0000000..c8ed04c
--- /dev/null
+++ b/docs/installation/application.md
@@ -0,0 +1,33 @@
+# L'application PowerSync
+
+Guide d'utilisation de l'application ETM PowerSync pour ajouter et configurer des appareils.
+
+## Accéder à l'interface
+
+L'interface est accessible depuis un navigateur sur le réseau local :
+
+```
+http://
+```
+
+!!! note "Capture"
+ *Placeholder — capture du menu principal de l'application à ajouter.*
+
+## Ajouter un appareil
+
+1. Ouvrir l'application et naviguer vers **Appareils** → **Ajouter un appareil**
+2. Sélectionner le type d'appareil dans la liste
+3. Renseigner les paramètres de connexion (adresse IP ou port série, adresse Modbus)
+4. Valider — l'appareil apparaît dans le tableau de bord si la connexion est établie
+
+!!! note "Capture"
+ *Placeholder — capture de l'écran de configuration d'un appareil
+ (`app-config-thing.png`) à remplacer quand l'app sera prête.*
+
+## Tableau de bord
+
+Le tableau de bord affiche en temps réel les puissances, l'état des bornes et le bilan
+énergétique du site.
+
+!!! note "Capture"
+ *Placeholder — capture du tableau de bord à ajouter.*
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
index 95b928c..a8e0c12 100644
--- a/docs/stylesheets/extra.css
+++ b/docs/stylesheets/extra.css
@@ -11,9 +11,21 @@
}
[data-md-color-scheme="slate"] .md-typeset a { color: var(--md-primary-fg-color); }
-/* badges de statut : {.badge .ok} / {.badge .part} / {.badge .road} */
+/* badges — 3 familles : canal APT · stabilité plugin · maturité fonctionnalité */
.badge{font-family:"IBM Plex Mono",monospace;font-size:.7rem;letter-spacing:.06em;
- text-transform:uppercase;padding:2px 8px;border-radius:20px;font-weight:600;white-space:nowrap}
-.badge.ok{color:#3fd18a;background:rgba(63,209,138,.12);border:1px solid rgba(63,209,138,.3)}
-.badge.part{color:#fec113;background:rgba(254,193,19,.1);border:1px solid rgba(254,193,19,.3)}
-.badge.road{color:#8fa9b5;background:rgba(143,169,181,.1);border:1px solid rgba(143,169,181,.3)}
+ text-transform:uppercase;padding:2px 8px;border-radius:20px;font-weight:600;white-space:nowrap;
+ display:inline-block;vertical-align:middle;line-height:1.4}
+
+/* canal APT */
+.badge.stable {color:#3fd18a;background:rgba(63,209,138,.12);border:1px solid rgba(63,209,138,.3)}
+.badge.testing {color:#fec113;background:rgba(254,193,19,.1);border:1px solid rgba(254,193,19,.3)}
+.badge.nightly {color:#8fa9b5;background:rgba(143,169,181,.1);border:1px solid rgba(143,169,181,.3)}
+
+/* stabilité plugin (meta.json) */
+.badge.consumer {color:#31a3dd;background:rgba(49,163,221,.1);border:1px solid rgba(49,163,221,.3)}
+.badge.community {color:#a78bfa;background:rgba(167,139,250,.1);border:1px solid rgba(167,139,250,.3)}
+
+/* maturité fonctionnalités (à la main) */
+.badge.ok {color:#3fd18a;background:rgba(63,209,138,.12);border:1px solid rgba(63,209,138,.3)}
+.badge.part {color:#fec113;background:rgba(254,193,19,.1);border:1px solid rgba(254,193,19,.3)}
+.badge.road {color:#8fa9b5;background:rgba(143,169,181,.1);border:1px solid rgba(143,169,181,.3)}
diff --git a/mkdocs.yml b/mkdocs.yml
index edea4be..19efee3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -22,8 +22,7 @@ theme:
primary: custom # couleurs réelles définies dans stylesheets/extra.css
accent: custom
features:
- - navigation.sections
- - navigation.tabs
+ - navigation.indexes
- navigation.top
- navigation.instant
- navigation.footer
@@ -47,6 +46,7 @@ markdown_extensions:
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.superfences
+ - pymdownx.details
- pymdownx.inlinehilite
- pymdownx.tabbed:
alternate_style: true
@@ -57,6 +57,8 @@ markdown_extensions:
plugins:
- search:
lang: fr
+ - literate-nav:
+ nav_file: SUMMARY.md
nav:
- Accueil: index.md
@@ -64,22 +66,18 @@ nav:
- installation/index.md
- Dépôt APT: installation/depot-apt.md
- Configuration: installation/configuration.md
+ - L'application: installation/application.md
+ - Appareils: appareils/
- Fonctionnalités:
- fonctionnalites/index.md
- Surplus solaire: fonctionnalites/surplus-solaire.md
- - Délestage / Load management: fonctionnalites/delestage.md
+ - Délestage: fonctionnalites/delestage.md
- Gestion batterie: fonctionnalites/gestion-batterie.md
- - API REST / MQTT: fonctionnalites/api.md
- - Appareils:
- - appareils/index.md
- - Compatibilité: appareils/compatibilite.md
- - Bornes de recharge: appareils/bornes.md
- - Compteurs: appareils/compteurs.md
- - Onduleurs / PV: appareils/onduleurs.md
- - Intégrations:
+ - Tarifs dynamiques: tarifs.md
+ - Référence:
+ - reference.md
- API REST: integrations/rest-api.md
- MQTT: integrations/mqtt.md
- - Tarifs: tarifs.md
- - Référence: reference.md
- - Dépannage: depannage.md
- - FAQ: faq.md
+ - Aide:
+ - Dépannage: depannage.md
+ - FAQ: faq.md
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..0716a54
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+mkdocs-material>=9.5
+mkdocs-literate-nav>=0.6
+PyYAML>=6.0
diff --git a/scripts/gen_device_reference.py b/scripts/gen_device_reference.py
new file mode 100644
index 0000000..22c1e78
--- /dev/null
+++ b/scripts/gen_device_reference.py
@@ -0,0 +1,409 @@
+#!/usr/bin/env python3
+"""
+gen_device_reference.py — génère la matrice de compatibilité, les sections de
+référence des appareils et le fichier de nav literate-nav (SUMMARY.md) depuis
+PORTING_STATUS.yaml + integrationplugin*.json + meta.json.
+
+Principe docs-as-code : ne remplace que le contenu entre marqueurs existants.
+
+
+ ... contenu régénéré ...
+
+
+Marqueur spécial pour la matrice de compatibilité :
+
+
+Usage :
+ python3 scripts/gen_device_reference.py --src .plugins-src --docs docs --lang fr
+ python3 scripts/gen_device_reference.py --src .plugins-src --docs docs --lang fr --check
+"""
+from __future__ import annotations
+import argparse
+import json
+import re
+import sys
+from pathlib import Path
+
+try:
+ import yaml
+except ImportError:
+ sys.exit("PyYAML est requis : pip install PyYAML")
+
+INTERFACE_ROLE = {
+ "energymeter": "Compteur d'énergie",
+ "smartmeterconsumer": "Compteur de consommation",
+ "smartmeterproducer": "Compteur de production",
+ "evcharger": "Borne de recharge",
+ "heatpump": "Pompe à chaleur",
+ "smartmeter": "Compteur intelligent",
+}
+TECH_INTERFACES = {"connectable", "networkdevice"}
+
+CREATE_METHOD = {
+ "discovery": "Découverte automatique",
+ "user": "Ajout manuel",
+ "auto": "Automatique",
+}
+
+CATEGORY_LABELS = {
+ "compteur": "Compteurs",
+ "irve": "Bornes de recharge",
+ "smartdevice": "SmartDevices",
+ "hvac": "HVAC",
+ "onduleur": "Onduleurs / PV",
+ "batterie": "Batteries / ESS",
+ "tarif": "Tarifs & prévisions",
+}
+
+CATEGORY_FOLDER = {
+ "compteur": "compteurs",
+ "irve": "bornes",
+ "smartdevice": "smart",
+ "hvac": "hvac",
+ "onduleur": "onduleurs",
+ "batterie": "batteries",
+ "tarif": "tarifs",
+}
+
+CATEGORY_ORDER = ["compteur", "irve", "smartdevice", "hvac", "onduleur", "batterie", "tarif"]
+
+CHANNEL_BADGES = {
+ "stable": 'STABLE',
+ "testing": 'TESTING',
+ "nightly": 'NIGHTLY',
+}
+
+STABILITY_BADGES = {
+ "consumer": 'CONSUMER',
+ "community": '',
+}
+
+MARKER_RE = re.compile(
+ r"()(?P.*?)()",
+ re.DOTALL,
+)
+
+
+# ── Chargement ────────────────────────────────────────────────────────────────
+
+def load_porting_status(root: Path) -> list:
+ path = root / "PORTING_STATUS.yaml"
+ if not path.exists():
+ sys.exit(f"ERREUR : PORTING_STATUS.yaml introuvable à {path}")
+ with open(path) as f:
+ return yaml.safe_load(f)
+
+
+def load_plugins(src: Path) -> dict:
+ """Charge tous les integrationplugin*.json trouvés (récursivement)."""
+ plugins: dict = {}
+ for f in sorted(src.rglob("integrationplugin*.json")):
+ if f.name not in plugins:
+ plugins[f.name] = json.loads(f.read_text(encoding="utf-8"))
+ return plugins
+
+
+def load_meta(src: Path, plugin: str) -> dict:
+ """Charge meta.json depuis le même dossier que integrationplugin.json."""
+ for f in sorted(src.rglob(f"integrationplugin{plugin}.json")):
+ meta_path = f.parent / "meta.json"
+ if meta_path.exists():
+ return json.loads(meta_path.read_text(encoding="utf-8"))
+ return {}
+ return {}
+
+
+# ── Libellé d'un appareil ────────────────────────────────────────────────────
+
+def entry_name(e: dict, meta: dict) -> str:
+ """Titre : PORTING_STATUS.name > meta.json.title > plugin (fallback dernier recours)."""
+ return e.get("name") or meta.get("title") or e.get("plugin", "?")
+
+
+# ── Helpers JSON nymea ────────────────────────────────────────────────────────
+
+def transport_of(tc: dict) -> str:
+ params = {p["name"] for p in tc.get("paramTypes", [])} | \
+ {p["name"] for p in tc.get("discoveryParamTypes", [])}
+ ifaces = set(tc.get("interfaces", []))
+ if "networkdevice" in ifaces or {"hostName", "address", "port", "macAddress"} & params:
+ return "Modbus TCP"
+ if {"modbusMasterUuid", "rtuMaster"} & params:
+ return "Modbus RTU"
+ return "—"
+
+
+def roles(tc: dict) -> str:
+ r = [INTERFACE_ROLE.get(i, i) for i in tc.get("interfaces", []) if i not in TECH_INTERFACES]
+ return ", ".join(r) if r else "—"
+
+
+def add_method(tc: dict) -> str:
+ return " / ".join(CREATE_METHOD.get(m, m) for m in tc.get("createMethods", [])) or "—"
+
+
+def resolve_protocol(plugin_data: dict | None) -> str:
+ if not plugin_data:
+ return "—"
+ for v in plugin_data.get("vendors", []):
+ for tc in v.get("thingClasses", []):
+ t = transport_of(tc)
+ if t != "—":
+ return t
+ return "—"
+
+
+# ── Rendu Markdown ────────────────────────────────────────────────────────────
+
+def fmt_range(p: dict) -> str:
+ lo, hi = p.get("minValue"), p.get("maxValue")
+ if lo is not None and hi is not None:
+ return f"{lo}–{hi}"
+ return "—"
+
+
+def fmt_default(p: dict) -> str:
+ d = p.get("defaultValue", "")
+ if d == "" or d is None:
+ return "—"
+ return f"`{d}`"
+
+
+def md_table(headers: list, rows: list) -> str:
+ out = ["| " + " | ".join(headers) + " |",
+ "| " + " | ".join("---" for _ in headers) + " |"]
+ for r in rows:
+ out.append("| " + " | ".join(str(c) for c in r) + " |")
+ return "\n".join(out)
+
+
+def render_params(tc: dict) -> list:
+ lines = []
+ disc = tc.get("discoveryParamTypes", [])
+ if disc:
+ rows = [[f"`{p['name']}`", p.get("displayName", ""), p.get("type", ""),
+ fmt_range(p), fmt_default(p)] for p in disc]
+ lines.append("_Paramètres de découverte :_")
+ lines.append(md_table(["Clé", "Libellé", "Type", "Plage", "Défaut"], rows))
+ lines.append("")
+ settings = tc.get("paramTypes", [])
+ if settings:
+ rows = [[f"`{p['name']}`", p.get("displayName", ""), p.get("type", ""),
+ fmt_range(p), fmt_default(p),
+ "oui" if p.get("readOnly") else "non"] for p in settings]
+ lines.append("_Réglages :_")
+ lines.append(md_table(["Clé", "Libellé", "Type", "Plage", "Défaut", "Lecture seule"], rows))
+ lines.append("")
+ return lines
+
+
+def render_states(tc: dict) -> list:
+ states = tc.get("stateTypes", [])
+ if not states:
+ return []
+ rows = [[f"`{s['name']}`", s.get("displayName", ""), s.get("type", ""),
+ s.get("unit") or "—"] for s in states]
+ return [md_table(["Clé", "Grandeur", "Type", "Unité"], rows), ""]
+
+
+def render_plugin(plugin: dict) -> str:
+ out = []
+ vendors = plugin.get("vendors", [])
+ vname = ", ".join(v.get("displayName", v.get("name", "")) for v in vendors)
+ out.append(f"**Fabricant :** {vname} ")
+ out.append(f"**Plugin :** `{plugin.get('name', '')}`")
+ out.append("")
+
+ tcs = [(v, tc) for v in vendors for tc in v.get("thingClasses", [])]
+ rows = [[f"**{tc.get('displayName', tc.get('name'))}**",
+ roles(tc), transport_of(tc), add_method(tc),
+ str(len(tc.get("stateTypes", [])))]
+ for _, tc in tcs]
+ out.append("#### Modèles pris en charge")
+ out.append(md_table(["Modèle", "Rôle", "Transport", "Ajout", "Grandeurs"], rows))
+ out.append("")
+
+ out.append("#### Détail par modèle")
+ for _, tc in tcs:
+ title = tc.get("displayName", tc.get("name"))
+ out.append(f'??? abstract "{title} — `{tc.get("name")}`"')
+ body = []
+ body += render_params(tc)
+ if tc.get("stateTypes"):
+ body.append("_Grandeurs mesurées :_")
+ body += render_states(tc)
+ for ln in ("\n".join(body)).splitlines():
+ out.append(" " + ln if ln else "")
+ out.append("")
+ return "\n".join(out).rstrip() + "\n"
+
+
+def render_matrix(entries: list, plugins: dict, src: Path) -> str:
+ by_cat: dict = {}
+ for e in entries:
+ by_cat.setdefault(e.get("category", "autre"), []).append(e)
+
+ lines = []
+ ordered_cats = [c for c in CATEGORY_ORDER if c in by_cat]
+ ordered_cats += [c for c in by_cat if c not in CATEGORY_ORDER]
+
+ for cat in ordered_cats:
+ label = CATEGORY_LABELS.get(cat, cat.capitalize())
+ cat_rows = []
+ for e in by_cat[cat]:
+ fname = f"integrationplugin{e['plugin']}.json" if e.get("plugin") else None
+ plugin_data = plugins.get(fname) if fname else None
+ # nightly sans JSON → absent de la matrice et du nav
+ if e["channel"] == "nightly" and (not fname or fname not in plugins):
+ continue
+ meta = load_meta(src, e["plugin"]) if e.get("plugin") else {}
+ name = entry_name(e, meta)
+ protocol = resolve_protocol(plugin_data)
+ channel_badge = CHANNEL_BADGES.get(e["channel"], e["channel"])
+ stability = meta.get("stability", "")
+ stability_badge = STABILITY_BADGES.get(stability, "—") if stability else "—"
+ cat_rows.append(f"| {name} | {protocol} | {channel_badge} | {stability_badge} |")
+ if not cat_rows:
+ continue
+ lines.append(f"## {label}\n")
+ lines.append("| Marque / Modèle | Protocole | Canal | Stabilité |")
+ lines.append("|---|---|---|---|")
+ lines.extend(cat_rows)
+ lines.append("")
+
+ return "\n".join(lines)
+
+
+# ── SUMMARY.md pour mkdocs-literate-nav ──────────────────────────────────────
+
+def generate_summary(entries: list, docs: Path, src: Path, plugins: dict) -> None:
+ """Écrit docs/appareils/SUMMARY.md (nav literate-nav)."""
+ by_cat: dict = {}
+ for e in entries:
+ by_cat.setdefault(e.get("category", "autre"), []).append(e)
+
+ lines = ["* [Compatibilité](compatibilite.md)"]
+
+ ordered_cats = [c for c in CATEGORY_ORDER if c in by_cat]
+ ordered_cats += [c for c in by_cat if c not in CATEGORY_ORDER]
+
+ for cat in ordered_cats:
+ label = CATEGORY_LABELS.get(cat, cat.capitalize())
+ folder = CATEGORY_FOLDER.get(cat, cat)
+ cat_entries = []
+ for e in by_cat[cat]:
+ if not e.get("plugin"):
+ continue
+ fname = f"integrationplugin{e['plugin']}.json"
+ # nightly sans JSON → pas de fiche, pas d'entrée nav
+ if e["channel"] == "nightly" and fname not in plugins:
+ continue
+ meta = load_meta(src, e["plugin"])
+ name = entry_name(e, meta)
+ slug = e.get("slug") or e["plugin"]
+ cat_entries.append(f" * [{name}]({folder}/{slug}.md)")
+ if cat_entries:
+ lines.append(f"* [{label}]({folder}/index.md)")
+ lines.extend(cat_entries)
+
+ summary_path = docs / "appareils" / "SUMMARY.md"
+ summary_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
+ print(f"SUMMARY.md → {summary_path.relative_to(docs.parent)}")
+
+
+# ── Validation ────────────────────────────────────────────────────────────────
+
+def validate_entries(entries: list, plugins: dict, src: Path) -> None:
+ errors = []
+ for e in entries:
+ if not e.get("plugin"):
+ continue
+ fname = f"integrationplugin{e['plugin']}.json"
+
+ if e["channel"] != "nightly":
+ # JSON obligatoire pour stable / testing
+ if fname not in plugins:
+ errors.append(
+ f" BLOQUANT : {fname} introuvable dans --src "
+ f"(channel={e['channel']}, plugin={e['plugin']})"
+ )
+ # Libellé obligatoire
+ meta = load_meta(src, e["plugin"])
+ if not e.get("name") and not meta.get("title"):
+ errors.append(
+ f" BLOQUANT : libellé manquant pour plugin={e['plugin']} "
+ f"(ni PORTING_STATUS.name ni meta.json.title)"
+ )
+
+ if errors:
+ sys.exit("ERREURS BLOQUANTES :\n" + "\n".join(errors))
+
+
+# ── Traitement des fichiers MD ────────────────────────────────────────────────
+
+def process(docs: Path, entries: list, plugins: dict, src: Path, check: bool) -> int:
+ entry_by_plugin = {e["plugin"]: e for e in entries if e.get("plugin")}
+ changed = []
+
+ for md in sorted(docs.rglob("*.md")):
+ text = md.read_text(encoding="utf-8")
+ if "