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>
249 lines
12 KiB
Markdown
249 lines
12 KiB
Markdown
# 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 (T1–T14). 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¤tPowerPhaseA=$(($1/3))¤tPowerPhaseB=$(($1/3))¤tPowerPhaseC=$(($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 T1–T6 (montées de puissance).
|