diff --git a/AGENTS.md b/AGENTS.md index 6755b32..d9a82fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,8 +17,9 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie). | 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]` | -| 3c — EcsRelayAdapter + waterfall ECS | ✅ FAITE — suite 18/18 + charging 46/46 | `6298d5d`→`54ba229` | +| 3c — adaptateur ECS à paliers + waterfall ECS | ✅ FAITE — suite 18/18 + charging 46/46 | `6298d5d`→`54ba229` | | 3e — SgReadyAdapter | ✅ FAITE — suite 19/19 | `83d5ad9`→`d8079e8` | +| rév. 3 — frontière optimiseur↔routeur | ✅ FAITE — `RelayRouter`, `LoadConfig`/`LoadConfigStore`, kind `Stage` retiré | `5100674`, `e16aca4`, `5585a5c`, `7184fe4`, `88626cf` | **Détail 3b** : - `EnergyArbitrator : public SmartChargingManager` — justification dans `## DÉCISIONS DE DESIGN` @@ -30,25 +31,30 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie). - 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) -**Détail 3c (morceaux déjà compilés, 0 erreur / 0 warning)** : -- **Morceau 0** — `LoadAction.force=false` (bypass verrous sécurité) ✅ -- **Morceau 1** — `EcsRelayAdapter` (.h + .cpp) : pilote N Things powerswitch, - `applyRelayStage()`, verrous `minOnS/minOffS`, bypass si `force==true` ✅ - - Enregistrement explicite via `EnergyArbitrator::registerEcsAdapter()` (tests + config) -- **Morceau 2** — `buildContext()` : `SurplusMeter` brut (`exportW = max(0, -meter->currentPower())`), - `loads[]` EV + ECS, `SurpusPv` déféré 3d ✅ +**Détail 3c (clôturée, commits `6298d5d` waterfall → `54ba229` testMeterSilentFallback)** : +- `LoadAction.force = false` — bypass des verrous en repli sécurité. +- Adaptateur ECS à N paliers powerswitch (`applyRelayStage()`, verrous + `minOnS`/`minOffS`, bypass si `force == true`), enregistré explicitement auprès + de l'arbitre. **Classe remplacée depuis** : voir la ligne rév. 3 du tableau. +- `buildContext()` : `SurplusMeter` brut (`exportW = max(0, -meter->currentPower())`), + `loads[]` EV + charges pilotées ; `SurplusPv` déféré. +- Waterfall (tri priorité ASC = rang) + dispatch + watchdog L2 (mode dégradé + conservateur, planification suspendue) + `degradedMode`/notification + tests. +- **Correctif clé** `[3c-3-fix]` : surplus **net signé** (délestage en import) ; + clamp lock-aware ; **seam de temps unifié** (`now = ctx.timestamp`, + `lockWindow()` source unique) ; watchdog injectable + (`recordMeterUpdate`/`evaluateMeterFreshness`, déclencheurs sous + `#ifndef ENERGY_SIMULATION`). +- Tests : `testEcsSurplusPV` (4 régimes) + `testMeterSilentFallback` (stabilité + + reprise). Suite simulation **18/18**, charging **46/46**, plugin prod 0/0. +- **arm64 cross : NON vérifié dans le sandbox dev** (pas de toolchain / + Qt6-aarch64 / docker) → relève de l'infra de build CI (`etm-powersync-deploy`). + À confirmer là-bas. -**3c CLÔTURÉE** (commits `6298d5d` waterfall → `54ba229` testMeterSilentFallback) : -- Morceaux 3-7 faits. Waterfall ECS (tri priorité ASC = rang) + dispatch + watchdog L2 - (mode dégradé conservateur, planif suspendue) + degradedMode/notification + tests. -- **Correctif clé** `[3c-3-fix]` : surplus **net signé** (délestage en import) + **clamp - lock-aware** `minStage/maxStage` (protection compresseur) ; **seam de temps unifié** - (`now=ctx.timestamp`, `lockWindow()` source unique) ; watchdog injectable - (`recordMeterUpdate`/`evaluateMeterFreshness`, déclencheurs sous `#ifndef ENERGY_SIMULATION`). -- Tests : `testEcsSurplusPV` (4 régimes) + `testMeterSilentFallback` (stabilité + reprise). - Suite simulation **18/18**, charging **46/46**, plugin prod 0/0. -- **arm64 cross : NON vérifié dans le sandbox dev** (pas de toolchain/Qt6-aarch64/docker) - → relève de l'infra de build CI (`etm-powersync-deploy`). À confirmer là-bas. +> Le clamp lock-aware de 3c passait par `telemetry.minStage`/`maxStage`. Ces +> champs ont été retirés du `SurplusContext` en rév. 3 ; le verrou est aujourd'hui +> interne à l'adaptateur (`relayrouter.cpp:193-203`). Écarts connus entre cette +> cible et le code : `specs/spec_ecs.md` §1. **3e CLÔTURÉE** (commits `83d5ad9` types → `d8079e8` testSgReadySurplus) : - `SgReadyAdapter` : 4 états normés (kind:State), encodage 2 bits, `lockWindow` symétrique @@ -67,27 +73,42 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie). ### Ce que le moteur sait faire aujourd'hui - **Arbitrage central unique** : un budget de surplus net signé, cascade par **priorité** (rang). -- **Charges** : EV (proxy amont, décision B), **ECS** (paliers, `Stage`), **SG-Ready PAC** - (4 états, `State`) — ECS+PAC classables ensemble sur le budget partagé. -- **Sécurité** : protection compresseur (verrous lock-aware `minStage/maxState`, seam de temps - unifié) ; watchdog L2 (compteur muet >90 s → mode dégradé conservateur, planif suspendue, +- **Charges** : EV (proxy amont, décision B), **charges pilotées en watts** (kind `Setpoint` — + `RelayRouter` pour une combinaison de relais, `EtmVariableLoadAdapter` pour une consigne + continue), **SG-Ready PAC** (4 états, kind `State`) — toutes les charges non-EV classables + ensemble sur le budget partagé. +- **Sécurité** : protection compresseur (verrous `minOn`/`minOff` par charge tenus dans + l'adaptateur, seam de temps unifié `now = ctx.timestamp` ; fenêtre d'états + `minState`/`maxState` exposée au scheduler pour SG-Ready) ; watchdog L2 + (compteur muet >90 s → mode dégradé conservateur, planif suspendue, reprise par recalcul) ; `verifyOverloadProtection()` amont intacte ; `degradedMode` notifié. - **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). + — `LoadAction.force` (bypass sécurité L2), `telemetry.minState/maxState` (fenêtre de verrou + SG-Ready), `degradedMode` (notification), et la forme `relay-router` de `LoadConfig`. + `minStage`/`maxStage` ne sont plus à documenter : les champs ont été retirés du contexte en + rév. 3. À 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** (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`. - Note : la déclaration des adaptateurs est aujourd'hui **codée** (`energypluginnymea.cpp`), - pas configurable à chaud — cf. `docs/TEST_TERRAIN.md` §1.d. +- **Couche config priorités** (UI Flutter drag-and-drop) — cf. `## ROADMAP`. + Note : la déclaration des **charges pilotées** est configurable et persistée depuis + `7184fe4` (`LoadConfigStore`, RPC `Get`/`SetLoadConfig`, reconstruction à chaud sur + `changed()`). Reste codé en dur : le **`SgReadyAdapter` du banc** + (`energypluginnymea.cpp:66-76`) — cf. `docs/TEST_TERRAIN.md` §1.d. + **PRÉCONDITION à ce basculement** : retirer d'abord + `Q_ASSERT(m_stateRelays.contains(2))` (`sgreadyadapter.cpp:36`) au profit d'un refus + explicite. Cette assertion garde l'état 2 — le repli sûr du mode dégradé L2 — et + disparaît du binaire release (`QT_NO_DEBUG`). Tant que la construction est codée en + dur, elle ne peut pas échouer ; dès que l'état vient de la config, elle devient le seul + garde-fou, et il n'existera pas chez le client. **Avant le basculement, pas après** — + cf. `specs/spec_ecs.md` ECS-110. - **arm64** cross-compile validé en pré-déploiement (infra CI `etm-powersync-deploy`). - **`Doxyfile` + job CI `doxygen -W`** (automatiser l'audit doc). @@ -142,79 +163,36 @@ dpkg-deb -I powersync-energy-plugin-nymea_*.deb | grep -E "Architecture|Version| --- -## PLAN 3C (validé, morceaux 0-2 déjà compilés) +## PLAN 3C — CLÔTURÉ -Plan approuvé par Patrick. Corrections A (double déduction EV) et B (anti-clignotement ECS) -**intégrées** dans le design ci-dessous. +Le plan de mise en œuvre a été exécuté (`6298d5d` → `54ba229`) puis remanié en rév. 3. +Il n'est plus reproduit ici : son pseudocode nommait une classe supprimée +(`EcsRelayAdapter`) et un identifiant d'adaptateur qui n'a jamais existé dans le code +livré (`"relay-stages"` ; la valeur réelle est `"relay-router"`, `relayrouter.cpp:74`). +Autorité courante sur l'ECS : `specs/spec_ecs.md`. -### Morceaux déjà compilés (0 erreur/warning) +Deux décisions de ce plan restent **en vigueur** et ne doivent pas être redécidées. -| # | Fichier(s) | Ce qui a été fait | -|---|-----------|-------------------| -| 0 | `types/loadaction.h` | `bool force = false` — bypass verrous sécurité | -| 1 | `adapters/ecsrelayadapter.h/.cpp` | Adaptateur N-paliers powerswitch, anti-rebond, `applyRelayStage()`, `etm.pri` | -| 2 | `energyarbitrator.h/.cpp` | `buildContext()` : `SurplusMeter` brut, `loads[]` EV+ECS, `registerEcsAdapter()`, `m_ecsAdapters` | +### Correction A — déduction EV unique, dans le scheduler -### Morceaux à venir +`ctx.meter.exportW` est la mesure **brute** (règle absolue 8). Les consignes EV du cycle +courant ne sont pas encore visibles au compteur : leur puissance commandée non encore +mesurée est réservée dans `getPlan()`, après le proxy EV — jamais dans `buildContext()`. -**3 — Waterfall ECS dans `RuleBasedScheduler::getPlan()`** +Implémentation : `rulebasedscheduler.cpp:86-97`. +Exemple : PV 9 kW, EV stable 7360 W, export mesuré 1140 W → `evReservedW = 0`, +budget charge = 1140 W. -Après la boucle EV proxy, ajouter : +### Correction B — anti-clignotement par recrédit de la consommation courante -``` -// Déduction unique (correction A) — ctx.meter.exportW = mesure brute -evReservedW = Σ EV en charge dans slot.actions : max(0, commandedA×phases×230 − ev->currentPower()) -remainingSurplusW = max(0, ctx.meter.exportW − evReservedW) - -// Tri loads ECS par priorité ASC (priority 1 = servi en premier ; protocole §5 + annexe C) -pour chaque LoadContext lc où lc.adapter == "relay-stages" : - budgetCharge = remainingSurplusW + lc.telemetry.currentPowerW // correction B anti-clignotement - bestStage = palier le plus haut dont stages[i] ≤ budgetCharge - reason = "Surplus PV — ECS palier N (W)" ou "Surplus insuffisant — ECS éteint" - remainingSurplusW = remainingSurplusW + lc.telemetry.currentPowerW − stages[bestStage] - // Grid funding : dormant jusqu'à 3f (commente, n'implémente pas) -``` - -**4 — `syncAdapters()` extension + `applyActionsToAdapters(Slot)` dans `update()`** -- `syncAdapters()` : commentaire "découverte ECS via interface 'ecsrelay' déférée 3g" -- `applyActionsToAdapters(Slot)` : itère slot.actions, dispatche via `m_ecsAdapters` pour kind==Stage - -**5 — Watchdog L2** (cf. SAFETY.md §L2) -- `QTimer m_meterWatchdog` 30 s — picoté sur `powerBalanceChanged` SIGNAL pour `m_lastMeterUpdate` -- `onMeterWatchdogTick()` : si `now − m_lastMeterUpdate > 90 s` → `applyDegradedMode()` -- `applyDegradedMode()` : ECS stage 0 force=true + EV courant minimum, reason="Compteur muet..." - -**6 — `degradedMode()` + notification + INTERFACE.md** -- `virtual bool degradedMode() const` dans `SmartChargingManager` (retourne false, `// [ETM]`) -- Override dans `EnergyArbitrator` -- Champ `"degradedMode": bool` dans `ChargingSchedulesChanged` (additif, rétro-compatible) -- Mise à jour `docs/INTERFACE.md` + limite dans `docs/SAFETY.md` : "valeur strictement constante non détectée" - -**7 — Mock powerswitch + tests** -- Mock JSON : ThingClass `mockPowerSwitch` (état `power` bool, état `currentPower` double) -- `energytestbase.h` : `mockPowerSwitchThingClassId` -- `testEcsSurplusPV` : cas normal + cas "ECS déjà au palier 1, surplus stable → Y RESTE" (anti-clignotement) -- `testMeterSilentFallback` : compteur muet 90 s → mode dégradé → ECS off ; **vérifier que - l'ECS RESTE à 0 sur plusieurs cycles `update()` pendant le silence** (planification - suspendue, pas de rallumage sur cache mort) ; dégel → reprise normale + `degradedMode=false`. - Repli EV : seul un VE *déjà en charge* est clampé au minimum (pas d'activation forcée). - Seam de test : piloter `onMeterWatchdogTick()` / `m_lastMeterUpdate` sans 90 s d'horloge réelle. - -### Arithmétique du budget (corrections A + B) - -**Correction A — déduction EV unique, dans le scheduler** : -``` -exportW dans ctx.meter = mesure brute (invariant 8, protocole §5) -evReservedW = déduction dans getPlan() APRÈS proxy EV, avec m_chargingActions fraîches -Pas de déduction dans buildContext(). -``` -Exemple : PV 9 kW, EV stable 7360 W, export mesuré 1140 W → evReservedW=0, budget ECS=1140 W ✓ - -**Correction B — anti-clignotement par recrédit de la conso actuelle** : ``` budgetCharge = remainingSurplusW + lc.telemetry.currentPowerW ``` -Identique à SCM ~l.1245 pour l'EV. Sans ce recrédit : ECS palier 1 → export chute → palier 0 → oscillation. + +Sans ce recrédit : la charge monte d'un palier → l'export chute → elle redescend → +oscillation. Même mécanique que l'EV en amont. + +Implémentation : `rulebasedscheduler.cpp:186`. --- @@ -234,20 +212,68 @@ Identique à SCM ~l.1245 pour l'EV. Sans ce recrédit : ECS palier 1 → export │ 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) + ┌───────────┴────────────┐ + RuleBasedScheduler SocketScheduler [3d] + (in-process, V1, GPL) (client unix://|tcp:// + plan à 1 créneau repli rules si absent/mort) │ - │ 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) + │ ↓ LoadAction — enveloppe en W (Setpoint), + │ ou State (SG-Ready) + │ ↑ LoadContext — télémétrie + bornes de verrou + │ + ┌──────────┬─────────┴─────┬────────────────┬──────────────────┐ +EvAdapter RelayRouter EtmVariableLoad SgReadyAdapter BatteryAdapter + W → combi- Adapter W → état 1-4 contrainte + +[câblage naison de W → consigne setpoint W + différé relais, continue réseau + 3g] verrous [3f] + │ │ │ │ │ + └────────────┴──────────────┴─────────────────┴────────────────────┘ + │ + Things nymea + (evcharger · powerswitch · états + powerSetpoint/currentPowerW) ``` +**Légende** — sans marque : en place. `[3d]` `[3f]` : prévu, **non écrit** (cf. +`## DÉFÉRÉ`). `[3g]` : classe écrite, câblage différé (voir « EV » ci-dessous). + +**Contrat descendant.** Le scheduler distribue une **enveloppe en watts**. Le kind `Stage` +a été supprimé (`5100674`) : **aucun index de palier ni identifiant de relais ne descend — +seulement des watts, fussent-ils arrondis sur les paliers déclarés** +(`buildSetpointAction()`, `rulebasedscheduler.cpp:196-205`). Seul SG-Ready conserve un kind +`State` : 4 états normés constructeur, non exprimables en watts. + +**Contrat montant.** Chaque adaptateur remonte sa télémétrie **et les bornes que ses +verrous imposent au cycle courant**, afin que l'arbitre décrémente le budget du palier +réellement applicable. + +Écarts connus entre cette cible et le code : `specs/spec_ecs.md` §1. + +**Traduction, pas répartition.** Un adaptateur convertit une enveloppe en consigne +matérielle — `RelayRouter` retient la combinaison de relais dont la puissance approche +l'enveloppe par le bas (`relayrouter.cpp:181-191`), `EtmVariableLoadAdapter` module en +continu (`etmvariableloadadapter.cpp:91`), `SgReadyAdapter` mappe sur un état normé. Aucun +ne s'attribue de budget : la règle 2 est respectée. + +**Instanciation.** `RelayRouter` et `EtmVariableLoadAdapter` sont construits par +`EnergyArbitrator::rebuildLoadAdapters()` depuis `LoadConfigStore` (persisté, RPC +`Get`/`SetLoadConfig`, reconstruction à chaud) — `energyarbitrator.cpp:137-175`. +`SgReadyAdapter` est encore **codé en dur** (`energypluginnymea.cpp:66-76` : `ThingId` +K1/K2, paliers `{3: 1500, 4: 3000}`), à basculer sur la config avec la couche priorités. +Réserve sur `EtmVariableLoadAdapter` : il est exercé de bout en bout par +`testLoadConfigBuildsAdapters`, mais **aucun matériel réel n'implémente encore l'interface +`etmvariableload`** — seul le mock la porte +(`tests/mocks/plugins/energymocks/integrationpluginenergymocks.json:865-888`). + +**EV, limite beta.** `EvAdapter::applyAction()` est **implémenté** (`evadapter.cpp:62-94`) +mais **jamais appelé** : le dispatch ne route les `Setpoint` que vers les charges pilotées, +et les `EvAdapter` vivent dans une table séparée jamais parcourue +(`energyarbitrator.cpp:288`). Les décisions EV restent prises par les méthodes amont, en +proxy, **avant** le waterfall ; l'EV n'entre donc pas dans le tri par priorité. +`descriptor()` et `telemetry()` sont utilisés dès maintenant pour le `SurplusContext`. +**3g est un travail de câblage, pas d'écriture.** + 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, @@ -311,7 +337,8 @@ avant validation du design de la phase. 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, + 3c. adaptateur ECS à paliers (livré, puis remplacé par `RelayRouter` en rév. 3) ; + 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. @@ -326,8 +353,12 @@ avant validation du design de la phase. `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`. +`EvAdapter::applyAction()` est **implémenté** (`evadapter.cpp:62-94`) mais **jamais +appelé** : `applyActionsToAdapters()` ne route les `Setpoint` que vers les charges pilotées, +et les `EvAdapter` vivent dans une table séparée jamais parcourue +(`energyarbitrator.cpp:288`). `descriptor()` et `telemetry()` sont utilisés dès maintenant +pour le `SurplusContext`. **3g est un travail de câblage, pas d'écriture** — ne pas +planifier la réécriture d'un adaptateur qui existe. **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 @@ -391,13 +422,20 @@ ou un VE ont un **temps de fonctionnement minimum incompressible** : ce n'est pa confort, c'est de la **protection matérielle** (le court-cycling détruit le compresseur). **Séparation des responsabilités** : -- Le **scheduler** décide le palier idéal selon le budget (peut vouloir « palier 0 »). -- L'**adaptateur** borne ce choix via `minStage`/`maxStage` (fenêtre `lockWindow()` évaluée - au temps de cycle) : une charge verrouillée ON garde son palier ; l'import transitoire - est **borné par minOn**, pas illimité. Le scheduler clampe et décrémente le budget au - palier réel (puissance engagée non-coupable) → budget correct pour les charges suivantes. -- `minOnS`/`minOffS` sont des **paramètres par charge** (constructeur `EcsRelayAdapter`, - config installateur) — **jamais codés en dur**. +- Le **scheduler** décide l'enveloppe idéale selon le budget (peut vouloir 0 W). +- L'**adaptateur** borne ce choix par sa fenêtre de verrou, évaluée au temps de cycle : + une charge verrouillée ON garde son palier ; l'import transitoire est **borné par + minOn**, pas illimité. + - Charges en watts : la fenêtre est **interne** à l'adaptateur + (`RelayRouter::lockWindow()`, `relayrouter.cpp:193-203`) et n'est pas exposée au + scheduler. + - SG-Ready : la fenêtre **est** exposée, via `telemetry.minState`/`maxState` + (`surpluscontext.h:66-71`), et le scheduler clampe dessus + (`rulebasedscheduler.cpp:254-264`). +- `minOnS`/`minOffS` sont des **paramètres par charge**, lus depuis la configuration + persistée (`LoadConfig`, `energyarbitrator.cpp:162`) — **jamais codés en dur**. + +Écarts connus entre cette cible et le code : `specs/spec_ecs.md` §1. **Défauts indicatifs par type** (à affiner à la mise en service) : @@ -407,10 +445,10 @@ confort, c'est de la **protection matérielle** (le court-cycling détruit le co | Ballon thermodynamique / PAC | ~300–600 s | ~300 s | **protection compresseur** (anti court-cycling) | | SG-Ready PAC (3e) | `minStateHoldS` ~900 s | — | maintien d'état imposé constructeur | -**Seam de temps** : `minStage`/`maxStage` (décision) ET le verrou de `applyAction` +**Seam de temps** : la fenêtre de verrou (décision) ET le verrou de `applyAction` (exécution) partagent le **même `now = ctx.timestamp`** via `lockWindow()` — source unique, -divergence impossible par construction, injectable en simulation. Voir `iloadadapter.h` -(contrat « temps = paramètre, jamais l'horloge »). +divergence impossible par construction, injectable en simulation. Voir +`iloadadapter.h:30-35` (contrat « temps = paramètre, jamais l'horloge »). --- @@ -423,7 +461,7 @@ supérieures n'affecte pas les couches inférieures. Voir `docs/SAFETY.md` pour |--------|-----|------| | **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". | +| **L2** | Watchdog fraîcheur compteur (en place) | `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 | @@ -460,12 +498,19 @@ supérieures n'affecte pas les couches inférieures. Voir `docs/SAFETY.md` pour - **MANQUE pour des priorités réglables par le client** : - (a) **3g** : transplanter le VE dans le waterfall unifié → toutes les charges (VE, ECS, SG-Ready, batterie) classables ensemble. - - (b) **Couche « config priorités »** : exposer et persister le `priority` de chaque - charge via l'API JSON-RPC, modifiable depuis l'app Flutter (drag-and-drop des - priorités de l'UI). C'est le **pont moteur↔UI**, un morceau à part entière - (ni 3e ni 3g). -- **État actuel** : `priority` est fixé à la création de l'adaptateur (`register*Adapter`), - pas d'interface de réglage à chaud. + - (b) **Couche « config priorités »** — le pont moteur↔UI, un morceau à part entière + (ni 3e ni 3g), aujourd'hui **à moitié livré** : + - **FAIT côté moteur** (`7184fe4`) : `priority` est exposé et persisté par charge, + via `GetLoadConfig` / `SetLoadConfig` (`LoadConfigStore`), et appliqué à chaud — + `changed()` → `rebuildLoadAdapters()`. + - **RESTE côté app** : l'UI Flutter de réordonnancement (drag-and-drop), qui consomme + ces deux appels. Aucun travail moteur n'y est attaché. +- **État actuel** : pour les **charges pilotées** (`relay-router`, `etmvariableload`), + `priority` est un champ de la configuration persistée (`loadconfig.h:73`), transmis à + l'adaptateur lors de sa construction (`energyarbitrator.cpp:162,167`), et modifiable + **à chaud**. `register*Adapter` ne fixe plus rien : il enregistre un adaptateur déjà + construit. Restent hors config : le VE (fixé à `priority = 100`, `evadapter.cpp:24` — + cf. « Dette 3g ») et le `SgReadyAdapter` du banc (`energypluginnymea.cpp:66-76`). - **Argument démo nymea** : le client réordonne ses charges, le surplus suit. ## RÉFÉRENCES @@ -474,4 +519,8 @@ supérieures n'affecte pas les couches inférieures. Voir `docs/SAFETY.md` pour §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`). +- `specs/spec_ecs.md` — **autorité sur l'ECS multi-palier** : état des exigences après + audit, ordre de traitement, arbitrages ouverts. Subordonné à ce fichier. +- `specs/spec_loadmodel.md` — modèle de charges (domaine × mécanisme). + **Intention de conception : ne déclenche aucun travail.** - Carte globale du workspace : `../AGENTS.md`.