Patrick Schurig 6670bed6cc [3e+doc] clôture 3e + TEST_TERRAIN.md + point d'étape
docs/TEST_TERRAIN.md : procédure Palier 1 (14 tests T1-T14) pour le banc nymea-dev arm64
— §0 pré-vol (forçables [À LIRE SUR LA BOX]), §1 déploiement (cross-arm64→scp→dpkg,
logging NymeaEnergy.debug, déclaration adaptateurs codée dans energypluginnymea.cpp),
§2-3 ECS 1/3 relais (dont transition non-cascadée 1500→2000), §4 SG-Ready (montée/
atomicité/hystérésis), §5 watchdog L2, §6 interaction priorités, §7 EV optionnel.

AGENTS.md ÉTAT : 3e clôturée (testEcsRelayTopologies dfdd988), audit Doxygen (5→0),
TEST_TERRAIN créé ; déféré = passe README+contrats (force/minStage-maxStage/min-maxState/
degradedMode pas encore dans le protocole publié), Waveshare, V2C, 3f, 3g, config priorités,
arm64 CI, Doxyfile+CI. Prochaine action : test terrain vendredi puis passe contrats.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:31:47 +02:00

249 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TEST_TERRAIN.md — Procédure de test Palier 1 (nymea-dev arm64)
Test terrain du moteur **etm-powersync-energy-plugin-etm** sur banc : compteur **mock
forçable** (puissance injectée par HTTP) + **relais GPIO réels** (vérifiés par `gpioget`).
14 tests (T1T14). Chaque test : **inject → log attendu → vérif GPIO → case ✓/✗**.
> Convention puissance compteur : `currentPower < 0` = **export** (surplus PV) ; `> 0` = **import**.
> Le moteur calcule un surplus net SIGNÉ `(exportW importW)` qui cascade par priorité.
---
## §0 — Pré-vol (à remplir SUR LA BOX)
| Élément | Valeur | Source |
|---|---|---|
| IP nymea-dev | `__________` | **[À REMPLIR]** (réseau banc) |
| Port JSON-RPC | `nymeas://<IP>:2222` (TCP+TLS) · `wss://<IP>:4444` | connu |
| Auth requise ? | `__________` | **[À LIRE SUR LA BOX]** `JSONRPC.Hello` → champ `authenticationRequired` |
| Token (si auth) | `__________` | `Users.Authenticate {username,password,deviceName}``token` |
| ThingClassId compteur mock | `2721a051-6e12-471a-baba-21d87c4cebc9` | connu (energymocks) |
| ThingId compteur mock | `__________` | **[À LIRE]** après `Integrations.AddThing`, ou `Integrations.GetThings` |
| Port HTTP compteur mock | `26655` (param `port` par défaut) | connu |
| ThingId relais R500 / R1000 / R2000 | `__________` | **[À LIRE]** `Integrations.GetThings` (plugin GPIO relais réel) |
| ThingId relais SG-Ready K1 / K2 | `__________` | **[À LIRE]** idem |
| gpiochip + offsets relais | `__________` | **[À LIRE]** `gpiodetect` puis `gpioinfo gpiochip0` |
| Conteneur build cross-arm64 | `__________` | **[À LIRE DANS `etm-powersync-deploy`/DEPLOY.md]** |
> `[À LIRE SUR LA BOX]` = dépend du déploiement, pas inventé ici. Tout le reste est figé.
---
## §1 — Déploiement (option a : build cross-arm64 → scp → dpkg)
### a. Build cross-arm64 (depuis le poste dev)
```bash
# Dans le conteneur de build cross-arm64 (cf. DEPLOY.md du repo etm-powersync-deploy).
# Produit le .deb arm64 : nymea-energy-plugin-nymea_<ver>_arm64.deb
# (TARGET inchangé = libnymea_energypluginnymea.so, drop-in remplaçant l'amont).
```
> Le nom de paquet/TARGET est **inchangé** (décision Phase 1) → le `.deb` ETM **remplace**
> le plugin énergie amont. Un seul plugin énergie chargé.
### b. Déploiement sur la box
```bash
BOX=<IP> # [À REMPLIR]
scp nymea-energy-plugin-nymea_*_arm64.deb root@$BOX:/tmp/
ssh root@$BOX 'dpkg -i /tmp/nymea-energy-plugin-nymea_*_arm64.deb && systemctl restart nymead'
ssh root@$BOX 'journalctl -u nymead -f' # suivre les logs
```
> `.so` installé dans `/usr/lib/<multiarch>/nymea/energy/libnymea_energypluginnymea.so`.
### c. Activer le logging `[Arbitre]` (SINON aucune trace de décision)
La catégorie est exactement **`NymeaEnergy`** (`NYMEA_LOGGING_CATEGORY(dcNymeaEnergy, "NymeaEnergy")`).
Méthode robuste — drop-in systemd (Qt logging rules) :
```bash
ssh root@$BOX 'mkdir -p /etc/systemd/system/nymead.service.d && \
printf "[Service]\nEnvironment=QT_LOGGING_RULES=NymeaEnergy.debug=true\n" \
> /etc/systemd/system/nymead.service.d/etm-logging.conf && \
systemctl daemon-reload && systemctl restart nymead'
```
> Alternative selon la version : `nymead --logging <règles>` ou la section logging de
> `/etc/nymea/nymead.conf`. **[VÉRIFIER `nymead --help` SUR LA BOX]** si le drop-in ne suffit pas.
> Les lignes attendues commencent par `[Arbitre]` et `[EcsRelayAdapter]`/`[SgReadyAdapter]`.
### d. Déclarer les adaptateurs de test — LA VRAIE MÉTHODE
⚠️ **Il n'existe pas encore de config runtime des adaptateurs** (déféré : couche « config
priorités »). En production, `energypluginnymea.cpp::init()` crée l'`EnergyArbitrator` mais
**n'enregistre aucun adaptateur** ECS/SG-Ready. La déclaration se fait donc par un **bloc de
code** ajouté à `energypluginnymea.cpp` (juste après la création de `chargingManager`,
ligne ~54), recompilé dans le `.deb` de test.
Workflow : déployer une 1re fois → `Integrations.AddThing` le compteur mock + les relais GPIO
→ relever leurs ThingId (`GetThings`) → coller le bloc ci-dessous avec ces ThingId → rebuild → redéployer.
```cpp
// energypluginnymea.cpp, init(), APRÈS la ligne :
// EnergyArbitrator *chargingManager = new EnergyArbitrator(...);
#ifdef ETM_ARBITRATOR
{
ThingManager *tm = thingManager();
// --- ECS 3 relais (8 niveaux binaires) — ThingId réels [À REMPLIR] ---
const QString R500 = "{__________}"; // relais GPIO 500 W
const QString R1000 = "{__________}"; // relais GPIO 1000 W
const QString R2000 = "{__________}"; // relais GPIO 2000 W
auto *ecs = new EcsRelayAdapter(
tm, "ecs-terrain", "ECS banc",
QList<int>({0, 500, 1000, 1500, 2000, 2500, 3000, 3500}),
QList<QList<QString>>({ {}, {R500}, {R1000}, {R500,R1000},
{R2000}, {R500,R2000}, {R1000,R2000}, {R500,R1000,R2000} }),
/*minOnS*/ 60, /*minOffS*/ 60, /*priority*/ 1, chargingManager);
chargingManager->registerEcsAdapter(ecs);
// --- SG-Ready (2 bits K1/K2) — ThingId réels [À REMPLIR] ---
const QString K1 = "{__________}";
const QString K2 = "{__________}";
auto *pac = new SgReadyAdapter(
tm, "pac-terrain", "PAC banc",
QHash<int,QList<QString>>({ {1,{K1}}, {2,{}}, {3,{K2}}, {4,{K1,K2}} }),
QHash<int,double>({ {1,0.0}, {2,0.0}, {3,1500.0}, {4,3000.0} }),
/*minStateHoldS*/ 300, /*priority*/ 2, chargingManager);
chargingManager->registerSgReadyAdapter(pac);
}
#endif
```
> Inclure `#include "etm/adapters/ecsrelayadapter.h"` et `etm/adapters/sgreadyadapter.h`.
> Le **root meter** = le compteur mock : le déclarer via l'expérience énergie nymea
> (`Energy.SetRootMeter` / config) avec le ThingId du compteur mock.
> Pour l'ECS simple (T1/T2), utiliser un seul relais : `stages {0,2000}`, `mapping {{},{R2000}}`.
---
## Helpers bash (poste dev ou box)
```bash
BOX=<IP>; MPORT=26655 # [À REMPLIR]
# Injecter une puissance compteur (W). Négatif = export (surplus PV), positif = import.
inject(){ curl -s "http://$BOX:$MPORT/setstates?connected=true&currentPowerPhaseA=$(($1/3))&currentPowerPhaseB=$(($1/3))&currentPowerPhaseC=$(($1/3))" >/dev/null; echo "compteur = $1 W"; }
# Compteur MUET : on cesse d'injecter (>90 s) → watchdog L2 bascule (QTimer 30 s, seuil 90 s).
mute(){ echo "NE PLUS injecter pendant >90 s…"; sleep 95; }
# Lire un relais GPIO réel (0/1) : relay <gpiochip> <offset>
relay(){ ssh root@$BOX "gpioget $1 $2"; } # ex. relay gpiochip0 17
# Suivre les décisions
logs(){ ssh root@$BOX "journalctl -u nymead -f | grep -E 'Arbitre|EcsRelay|SgReady'"; }
```
> Le compteur mock pose `currentPower = somme des 3 phases`. `RootMeter::currentPower()` le relit.
---
## §2 — ECS simple (1 relais, paliers {0, 2000})
### T1 — Montée sur surplus
- **inject** : `inject -2500`
- **log attendu** : `[Arbitre] … Surplus PV … ECS palier 1` puis `[EcsRelayAdapter] … → stage 1`
- **vérif GPIO** : `relay <chip> <R2000>`**1** (ON)
- ✓ / ✗ : `____`
### T2 — Délestage (import)
- **inject** : `inject 1000` (import → surplus net négatif)
- **log attendu** : `Surplus insuffisant … ECS éteint` ; `→ stage 0`
- **vérif GPIO** : relais → **0** (OFF)
- ✓ / ✗ : `____`
---
## §3 — ECS 3 relais (8 niveaux binaires R500/R1000/R2000)
### T3 — Cascade montante
- **inject** : `inject -1700` (budget 1700 → palier **1500**)
- **log** : `ECS palier 3 (1500 W)`
- **vérif GPIO** : R500=**1**, R1000=**1**, R2000=**0**
- ✓ / ✗ : `____`
### T4 — Transition NON-CASCADÉE 1500 → 2000 (le test clé)
- **contexte** : on part de T3 (palier 1500, l'ECS mesure 1500 W).
- **inject** : `inject -700` (budget = 700 + 1500 recrédit = 2200 → palier **2000**)
- **log** : `ECS palier 4 (2000 W)` ; commutation **off-before-on** (R500/R1000 coupés avant R2000)
- **vérif GPIO** : R500=**0**, R1000=**0**, R2000=**1** ← *set final R2000 SEUL, pas un état parasite durable*
- ✓ / ✗ : `____`
### T5 — Protection minOn (anti court-cycling)
- **contexte** : ECS vient de commuter (< minOn 60 s).
- **inject** : `inject 1200` (import le budget voudrait éteindre)
- **log attendu** : `Verrou minOn — … maintenu palier …`
- **vérif GPIO** : relais **inchangés** (maintien). Attendre > 60 s puis re-`inject 1200` → délestage.
- ✓ / ✗ : `____`
### T6 — Délestage complet
- **inject** : `inject 2000` (import franc, > minOn écoulé)
- **vérif GPIO** : R500=R1000=R2000=**0**
- ✓ / ✗ : `____`
---
## §4 — SG-Ready (PAC, 2 bits K1/K2 ; P3=1500, P4=3000)
### T7 — Montée d'états 2 → 3 → 4
- **inject** `-1000` → état **2** (K1=0,K2=0) ; **inject** `-2000` → état **3** (K2=1) ;
attendre > minStateHold (300 s) puis **inject** `-2500` → état **4** (K1=1,K2=1).
- **log** : `… recommandée (état 3 …)` puis `… forcée (état 4 …)`
- **vérif GPIO** : état 3 → K1=0,K2=1 ; état 4 → K1=1,K2=1
- ✓ / ✗ : `____`
### T8 — Atomicité (transitoire bénin)
- **contexte** : transition 2→4 (00→11) commute K1 ET K2.
- **vérif GPIO** : set **final** K1=**1**, K2=**1** (état 4). Le transitoire passe par K2 d'abord
(01=reco), **jamais** 10=blocage — sub-ms, non observable à `gpioget` mais **garanti code**
(`transientHarm`). Confirmer simplement le set final correct.
- ✓ / ✗ : `____`
### T9 — Hystérésis (pas de bascule 3↔4)
- **contexte** : PAC en état 4 ; faire osciller le surplus dans la zone morte.
- **inject** `-300` (budget ≈ 3300) → reste **4** ; `-500` (≈3500) → reste **4** ; `-100` (≈3100) → reste **4**.
- **vérif GPIO** : K1=K2=**1** stable (aucun clignotement 3↔4). Passer `inject 300` (budget < 3000) sort en 3.
- / : `____`
---
## §5 — Watchdog L2 (compteur muet)
### T10 — Compteur muet → mode dégradé
- **inject** `-2500` (ECS/PAC servis), puis **`mute`** (>90 s sans injection).
- **log attendu** : `[Arbitre] Compteur muet depuis … mode dégradé L2` ; ECS palier 0 ; PAC **état 2**.
- **vérif GPIO** : relais ECS → **0** ; PAC K1=0,K2=0 (état 2, **jamais** blocage).
- ✓ / ✗ : `____`
### T11 — Stabilité (faux surplus piège)
- **contexte** : toujours muet ; la **dernière** valeur injectée (-2500) reste « collée » au compteur.
- **vérif GPIO** : sur plusieurs minutes muettes, ECS **reste 0** malgré ce surplus stale
(planification suspendue — pas de replanif sur cache mort).
- ✓ / ✗ : `____`
### T12 — Reprise
- **inject** `-2500` (le compteur re-parle).
- **log attendu** : `Compteur de nouveau actif — sortie du mode dégradé` ; recalcul normal.
- **vérif GPIO** : ECS resuit le surplus (palier > 0). Pas de restauration d'ancienne consigne.
- ✓ / ✗ : `____`
---
## §6 — Interaction budget partagé
### T13 — Ordre de priorité ECS↔PAC inversable
- **contexte** : surplus moyen `inject -3000`, ECS palier 2400 vs PAC P3 1500.
- **ECS prio 1 / PAC prio 2** : ECS se sert (palier), PAC voit le reliquat (600) → **état 2**.
- **Inverser les priorités** (échanger `priority` dans le bloc §1.d, rebuild/redeploy) →
PAC se sert (état 3), ECS voit le reliquat (1500 < 2400) **palier 0**.
- **vérif GPIO** : le service s'inverse selon la priorité (preuve du waterfall unifié).
- / : `____`
---
## §7 — OPTIONNEL EV / V2C (si plugin prêt)
### T14 — Ordre étagé EV → ECS (beta : EV servi avant le waterfall)
- **contexte** : borne EV branchée + surplus moyen.
- **attendu** : l'EV est servi **en premier** (proxy amont, décision B), l'ECS voit le reliquat.
- **vérif** : courant EV puis palier ECS sur le reliquat. *(V2C = session dédiée, hors Palier 1.)*
- / : `____`
---
## Checklist sécurité (AVANT mise sous tension)
- [ ] **Disjoncteur L0** du banc **accessible** et identifié (coupure physique immédiate).
- [ ] Banc câblé hors tension ; sections/calibres relais conformes aux puissances (R2000 = 2000 W).
- [ ] **Polarité / repérage K1/K2** SG-Ready vérifié (1:0=blocage, 0:0=normal, 0:1=reco, 1:1=forcé).
- [ ] Relais résistifs ECS : pas de charge inductive sur ces voies.
- [ ] `gpioinfo` relevé et **mapping offset↔relais consigné** avant tout test.
- [ ] L1 (failsafe bornes/relais) configuré si applicable ; L0 reste le filet ultime.
- [ ] Un opérateur à la main sur le disjoncteur pendant T1T6 (montées de puissance).