diff --git a/AGENTS.md b/AGENTS.md index 9c7d201..bb7a63b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,9 +58,12 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie). normal ; état 1 jamais via surplus) + **waterfall UNIFIÉ** ECS+SG-Ready (un seul budget, trié par priorité). Mode dégradé L2 → état 2 (mains off, jamais blocage ; SAFETY.md corrigé). - Tests : `testSgReadySurplus` (montée · hystérésis · court-cycling · **budget partagé ECS↔PAC - avec inversion de priorité**). Suite simulation **19/19**, plugin prod 0/0. -- **DoD 3e** : amd64 0/0 ✓ · simulation 19/19 ✓ · `decisionReason` français (forcé/reco/normal/ + avec inversion de priorité**) + `testEcsRelayTopologies` (ECS 1 relais + 3 relais + **non-cascadé** 1500→2000, off-before-on) — commit `dfdd988`. +- **DoD 3e** : amd64 0/0 ✓ · simulation **20/20** ✓ · `decisionReason` français (forcé/reco/normal/ verrou) ✓ · arm64 → CI (idem 3c). +- **Audit Doxygen** fait (5 findings → 0 : `\param now`, docs périmées 3c/3e) — commit `51760a7`. +- **`docs/TEST_TERRAIN.md`** créé : procédure Palier 1 (14 tests) pour le banc nymea-dev arm64. ### Ce que le moteur sait faire aujourd'hui - **Arbitrage central unique** : un budget de surplus net signé, cascade par **priorité** (rang). @@ -72,14 +75,24 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie). - **Local-first** : zéro cloud (invariant 10). ### DÉFÉRÉ (ordre indicatif) +- **Passe README + contrats** : `OPTIMIZER_PROTOCOL.md` ne reflète PAS encore plusieurs ajouts + 3c/3e — `LoadAction.force` (bypass sécurité L2), `telemetry.minStage/maxStage` et + `minState/maxState` (fenêtres de verrou), `degradedMode` (notification). À documenter dans + le protocole publié + README architecture. **Prochaine session doc** (pas avant le terrain). - **Waveshare D8** : plugin DEVICE (8 powerswitch) sous l'adaptateur — **session dédiée** - (nouveau chantier transport Modbus). + (chantier transport Modbus RTU/RS485). +- **V2C** (borne EV) : intégration — **session dédiée**. - **3d** SocketScheduler (handshake/heartbeat/repli optimiseur). - **3f** BatteryAdapter (constraints + charge réseau plafonnée) + waterfall grid-funding. - **3g** transplantation EV dans le waterfall unifié → toutes charges classables ensemble. - **Couche config priorités** (API JSON-RPC + UI Flutter drag-and-drop) — cf. `## ROADMAP`. -- **arm64** cross-compile (pré-déploiement, infra CI) + **test terrain vendredi** (Palier 1, - things génériques sur `nymea-dev` arm64). + Note : la déclaration des adaptateurs est aujourd'hui **codée** (`energypluginnymea.cpp`), + pas configurable à chaud — cf. `docs/TEST_TERRAIN.md` §1.d. +- **arm64** cross-compile validé en pré-déploiement (infra CI `etm-powersync-deploy`). +- **`Doxyfile` + job CI `doxygen -W`** (automatiser l'audit doc). + +**PROCHAINE ACTION** : **test terrain vendredi** (`docs/TEST_TERRAIN.md`, Palier 1), puis +**passe contrats** (OPTIMIZER_PROTOCOL + README). **Remotes git** : - `origin` (`https://git.etm-powersync.fr/...`) = remote de travail — push normal diff --git a/docs/TEST_TERRAIN.md b/docs/TEST_TERRAIN.md new file mode 100644 index 0000000..c5acbae --- /dev/null +++ b/docs/TEST_TERRAIN.md @@ -0,0 +1,248 @@ +# 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://:2222` (TCP+TLS) · `wss://: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__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= # [À 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//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 ` 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({0, 500, 1000, 1500, 2000, 2500, 3000, 3500}), + QList>({ {}, {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>({ {1,{K1}}, {2,{}}, {3,{K2}}, {4,{K1,K2}} }), + QHash({ {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=; 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 +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 ` → **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).