# AGENTS.md — etm-powersync-energy-plugin-etm Moteur HEMS. Fork GPL de `nymea-energy-plugin-nymea`, étendu de l'optimisation EV vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie). - **Licence** : GPL-3.0 · **Miroir public** : OUI - **Branche de travail** : `feature/beta-rulebased` - **Document d'interface faisant autorité** : `docs/OPTIMIZER_PROTOCOL.md` (le contrat stratégie/arbitrage — interne ET socket). `INTERFACE.md` fait autorité sur l'API JSON-RPC. ## ÉTAT | Phase | Statut | Commit(s) | |-------|--------|-----------| | 0 — analyse fork / structure | ✅ FAITE | `f4d5b20` | | 1 — renommage .pro + métadonnées debian | ✅ FAITE | `f4d5b20` | | 2 — design arbitre validé | ✅ FAITE | `074fa71` | | 3a — structs protocole + interfaces | ✅ FAITE | `4ae1939` | | 3b — EnergyArbitrator + scheduler + adapter | ✅ FAITE — iso-fonctionnalité prouvée | `5f49e4c`, `d8ebd65`, `[3b-iv]` | **Détail 3b** : - `EnergyArbitrator : public SmartChargingManager` — justification dans `## DÉCISIONS DE DESIGN` - `EvAdapter` + `RuleBasedScheduler` implémentés - Build : **0 erreur / 0 warning** - `ETM_ARBITRATOR` **actif** dans `energyplugin.pri` - Iso-fonctionnalité prouvée : - Simulation : 226 lignes décisions identiques (Theoretically / Surplus / Current load), diff = 0 - Tests charging : 57 lignes décisions identiques, diff = 0 ; 46/46 PASS ref ET ETM - [Arbitre] présents avec raisons françaises pour les 4 cas (idle, surplus PV, aWATTar, deadline) **PROCHAINE ACTION — 3c** : - `EcsRelayAdapter` (paliers 0/1/2) — premier adaptateur non-EV - Pipeline ETM waterfall (budget surplus → ECS) dans `EnergyArbitrator::update()` - Scénario simulation dédié : chauffe-eau sur surplus PV **Remotes git** : - `origin` (`https://git.etm-powersync.fr/...`) = remote de travail — push normal - `etm-public` (`gitea-lan:...powersync-energy-plugin-etm`) = miroir public GPL → push **MANUEL par Patrick uniquement** (`sync-public.sh`) - `etm-pro` = reliquat historique — ne pas utiliser, cartographie à clarifier --- > ⚠️ Tout plan antérieur mentionnant « créer etm/ avec PowerSyncClient et > StaticHcHpProvider comme première étape » ou « injecter l'optimiseur dans > SmartChargingManager » est **INVALIDE et ABANDONNÉ**. Ne pas le reprendre, > quelle qu'en soit la source (fichier, mémoire de session, contexte). --- ## ARCHITECTURE CIBLE (non négociable) ``` ┌──────────────────────────────┐ │ ARBITRAGE CENTRAL │ ← généralisation du │ budget de surplus UNIQUE │ SmartChargingManager amont │ waterfall par priorités │ └──────┬───────────────────────┘ │ IScheduler (= contrat OPTIMIZER_PROTOCOL) ┌───────────┴───────────┐ RuleBasedScheduler SocketScheduler (in-process, V1, GPL) (client unix://|tcp://, V1 aussi — plan à 1 créneau personne en face en beta : repli rules) │ │ distribue le budget en LoadAction typées ┌──────────┬────┴─────┬──────────────┐ EvAdapter EcsRelayAdapter SgReadyAdapter BatteryAdapter (setpoint, (stage 0/1/2) (state 1-4) (constraint + iface setpoint W réseau) evcharger nymea) ``` Règles absolues : 1. **UN seul arbitre.** Le budget de surplus est une ressource unique, arbitrée à UN endroit. **INTERDIT : managers frères par type de charge** (EcsManager, BatteryManager à côté du SmartChargingManager) — deux décideurs sur le même surplus = sur-engagement et oscillations. 2. **Les LoadAdapters exécutent, ils ne décident pas.** Un adaptateur : parle à son matériel, déclare ses capacités/contraintes (`declared`, `limits`, types d'action), expose sa télémétrie, applique les `LoadAction` reçues. Aucune logique de répartition dedans. 3. **Le SmartChargingManager amont est EV-spécifique** : il se GÉNÉRALISE en arbitrage multi-charges (`ChargingAction` → `LoadAction`, bornes EV → adaptateurs). On ne branche PAS l'optimiseur dans le manager EV tel quel. 4. **La boucle de sécurité est intouchable** : `verifyOverloadProtection()` (temps réel) + bornes par adaptateur écrêtent TOUTE sortie de stratégie, interne ou socket. 5. **Plan par créneaux** (OPTIMIZER_PROTOCOL §6) : seul le créneau courant est exécuté. Le rule-based répond un plan à 1 créneau. Modèle async = **cache** : le plan du cycle précédent s'applique, le recalcul se fait en fond. Jamais d'attente dans `update()`. 6. **Repli toujours fonctionnel** : optimiseur absent/mort/abstain → rule-based. Capabilities (`tier`, `optimizerExpected`, `optimizerAlive`, `activeStrategy`) reflètent l'état en continu. 7. **`decisionReason` non vide, en français, sur chaque action.** Action sans reason = rejetée. 8. **Pas de boucle de feedback** : surplus = PV mesurée + compteur, jamais le net après pilotage. 9. **Aucun composant propriétaire ici** (Héos = repo privé `etm-powersync-optimizer`). Ce repo doit compiler et tourner seul, GPL pur. ## RÉPONSES FIGÉES (ne plus poser ces questions) - Plages HC/HP et tarifs : **configuration JSON**, jamais hardcodé. Prévoir Tempo (6 types de jours), pas seulement HC/HP. - Async : **modèle cache** (cf. règle 5). - Bugs upstream : **commits séparés** du code ETM, message préfixé `[upstream-fix]`. Candidats PR nymea (fix phases EV, Keba) = patchs isolés, propres, upstreamables. - `protocolVersion` : **constante `"1.0"`**, pas un paramètre de config. - Renommage : FAIT (Phase 1, commit f4d5b20). TARGET et noms de paquets debian INCHANGÉS (.so drop-in remplaçant l.amont — garantit un seul plugin énergie chargé). ## WORKFLOW OBLIGATOIRE Chaque phase produit un livrable VALIDÉ PAR PATRICK avant la suivante. Jamais de code avant validation du design de la phase. - **Phase 0 — Analyse (en cours)** : répondre par écrit, code lu à l'appui : (a) quelles charges SmartChargingManager pilote-t-il (types manipulés) ; (b) ChargingAction peut-il exprimer « ECS palier 1 » / « batterie décharge interdite » — citer ses champs ; (c) avec des managers séparés, où vivrait le budget unique. Zéro code, zéro plan d'implémentation. - **Phase 1 — Renommage** : `git mv` du `.pro`, TARGET, debian/. Un commit, revue. - **Phase 2 — Design de l'arbitrage généralisé** : interface `LoadAdapter` (méthodes, ce qu'un adaptateur déclare), flux du budget, mapping `LoadAction`→adaptateurs, où vit `IScheduler`. Texte + signatures, pas d'implémentation. Validation Patrick. - **Phase 3 — Implémentation par étapes** (chacune : compile amd64 + cross arm64, et un scénario `docker-simulation.sh` qui la prouve = DoD) : 3a. structs du protocole (contexte, plan, actions) ; 3b. arbitre + RuleBasedScheduler + EvAdapter (iso-fonctionnel avec l'amont sur EV) ; 3c. EcsRelayAdapter (paliers) ; 3d. SocketScheduler (handshake/heartbeat/repli, testé contre un optimiseur factice ~50 lignes) ; 3e. SgReadyAdapter ; 3f. BatteryAdapter (constraints + charge réseau plafonnée). - **Bugs upstream** : au fil de l'eau, commits `[upstream-fix]` séparés. ## DÉCISIONS DE DESIGN (écarts et justifications) ### 3b révisé — délégation EV à l'amont (beta assumée) **Décision Patrick** : hybride étagé pour la beta. **En beta** : les décisions EV restent dans les méthodes amont `planSurplusCharging` / `planSpotMarketCharging` (`SmartChargingManager`), inchangées. `RuleBasedScheduler::getPlan()` les appelle en **proxy** et reformate leurs sorties (`ChargingActions`) en `LoadAction` pour le log `[Arbitre]`. `EvAdapter::applyAction()` est **inactif** jusqu'à 3g — mais `descriptor()` et `telemetry()` sont utilisés dès maintenant pour le `SurplusContext`. **Pipeline ETM réel** (waterfall budget Surplus/Grid, `applyAction`) arrive en **3c** pour les charges non-EV (ECS, SG-Ready), alimenté par le surplus *restant* après déduction de l'`addedPower` des consignes EV du cycle courant (pas encore visible au compteur). **Limitations beta assumées** : - EV toujours prioritaire ; waterfall appliqué uniquement aux charges non-EV. - Le classement drag-and-drop (priorités) ne portera que sur les charges non-EV. **Étape 3g (post-beta)** : transplantation réelle de la logique EV dans `RuleBasedScheduler` → priorités libres entre toutes les charges (EV, ECS, SG-Ready, batterie). --- ### 3b-iii — EnergyArbitrator hérite de SmartChargingManager **Design validé en session** : "nouvelle classe dans etm/, n'étend pas SmartChargingManager". **Écart implémenté** : `EnergyArbitrator : public SmartChargingManager`. **Justification** : 1. **Contrainte NymeaEnergyJsonHandler** : ce handler amont prend un `SmartChargingManager*` dans son constructeur. Sans héritage, toute solution propre (interface commune, pointeur générique) nécessiterait de modifier `nymeaenergyjsonhandler.h/.cpp` — violation de la règle "Modifier le code amont uniquement pour corriger des bugs". 2. **verifyOverloadProtection() intacte** : héritée bit-pour-bit, connectée aux mêmes signaux via le constructeur du parent. Zéro risque de régression sur la sécurité. 3. **simulationCallUpdate() polymorphe** : appelle `update()` virtuel → redirige automatiquement vers `EnergyArbitrator::update()`. Les tests amont passent sans modification. 4. **Minimal upstream diff** : seuls les attributs `protected`/`virtual` changent dans `smartchargingmanager.h` (marqués `// [ETM]`). Zéro logique upstream modifiée. **Risque accepté** : `EnergyArbitrator` a accès à l'état privé de SCM via les accesseurs `internal*`. La discipline AGENTS (LoadAdapters exécutent, ne décident pas ; un seul arbitre) compense. Si SCM était refactorisé en amont pour exposer une interface publique propre, l'héritage pourrait être remplacé par composition. --- ## MODÈLE DE SÉCURITÉ (décision Patrick — immuable) Cinq couches indépendantes. Chacune est conçue pour qu'une défaillance des couches supérieures n'affecte pas les couches inférieures. Voir `docs/SAFETY.md` pour le détail. | Couche | Qui | Quoi | |--------|-----|------| | **L0** | Disjoncteur / Linky matériel | Coupure physique — hors logiciel | | **L1** | Failsafe natif des bornes | Config installateur, checklist ETM | | **L2** | Watchdog fraîcheur compteur (à coder en 3c) | `QTimer` piloté : si `lastMeterUpdate > 90 s` → mode dégradé (EV min/off, ECS off, pas de charge réseau batterie), `decisionReason` explicite, notification nymea. Scénario simulation dédié : "compteur muet → repli". | | **L3** | Watchdog systemd sur nymead | Repo `etm-powersync-deploy`, hors scope ici | | **L4** | Logique signal-driven existante | Boucle `update()` déclenchée par événements | **Règles de code** : - Le watchdog L2 est piloté par **`QTimer`** (pas par signal `meterChanged`) pour rester actif même si le signal ne fire plus. - Mode dégradé = consignes **de repli** (EV au minimum si pluggedIn, ECS off, etc.) + `decisionReason` non vide + notification `EnergyManagerChanged` avec `degradedMode`. - `verifyOverloadProtection()` (L4) est déclenchée par **deux mécanismes** : (a) signal `powerBalanceChanged` (temps réel — SCM.cpp ligne 127, mécanisme principal) ; (b) appel cyclique en position 3 d'`update()` (SCM.cpp ligne 313, filet périodique). La position dans `update()` est **INTOUCHABLE** — même dans `EnergyArbitrator::update()`. --- ## DÉFINITION DE FAIT (par étape de phase 3) 1. Compile amd64 et cross arm64. 2. Scénario de simulation ajouté/étendu qui démontre le comportement (le harnais `docker-simulation.sh` + `tests/auto` hérités sont le banc de test). 3. `decisionReason` visibles dans les logs de simulation. 4. Aucune régression des tests amont existants. 5. Toute classe/méthode **publique** de `etm/` porte un commentaire Doxygen : `\brief`, `\param`, `\return`, et surtout le **contrat de comportement** (invariants, écrêtage, hypothèses que l'appelant peut faire). Les headers 3a servent de modèle — les convertir au format Doxygen lors du passage 3b. ## RÉFÉRENCES - `docs/OPTIMIZER_PROTOCOL.md` — le contrat. §5 (SurplusContext), §6 (plan/actions), §7 (repli), annexe C (priorités). - `README.md` — architecture (deux boucles, frontière), `etm_powersync_energy.svg`. - `INTERFACE.md` — API JSON-RPC existante (`NymeaEnergy`, cible future `Ems`). - Carte globale du workspace : `../AGENTS.md`.