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

12 KiB
Raw Blame History

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)

# 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

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) :

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.

// 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)

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=1set 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).