docs(agents): consolidation des zones périmées (12 zones, aucun code)
Purge des affirmations devenues fausses depuis la rév. 3. Chaque correction est
vérifiée contre le code ; rien n'est repris du texte existant par défaut.
Zones : ÉTAT (tableau + Détail 3c) · « ce que le moteur sait faire » · DÉFÉRÉ
(2 entrées) · PLAN 3C · ARCHITECTURE CIBLE · WORKFLOW phase 3 · 3b révisé ·
Verrous minOn/minOff (2 endroits) · MODÈLE DE SÉCURITÉ (L2) · ROADMAP ·
RÉFÉRENCES.
Principales remises au fait :
- EcsRelayAdapter et le kind Stage, supprimés en 5100674, disparaissent du
schéma « non négociable » au profit de RelayRouter + EtmVariableLoadAdapter ;
SocketScheduler et BatteryAdapter, qui n'existent pas, sont marqués [3d]/[3f].
- EvAdapter::applyAction() n'est pas « inactif » : il est implémenté
(evadapter.cpp:62-94) mais jamais appelé (energyarbitrator.cpp:288).
3g est un travail de câblage, pas d'écriture.
- minStage/maxStage ont été retirés du contexte ; minState/maxState et
minStateHold subsistent et sont conservés tels quels.
- La configuration des charges est persistée et rechargée à chaud depuis
7184fe4 — le point (b) de la ROADMAP est fait côté moteur, seule l'UI
Flutter reste ouverte.
- Le watchdog L2 est en place, plus « à coder en 3c ».
- PLAN 3C n'est plus reproduit (pseudocode nommant une classe supprimée et un
identifiant « relay-stages » jamais livré) ; les corrections A et B, toujours
en vigueur, sont conservées avec leurs références actuelles.
- PRÉCONDITION ajoutée au basculement de SgReadyAdapter sur la config :
retirer d'abord Q_ASSERT(m_stateRelays.contains(2)) au profit d'un refus
explicite — l'assertion garde l'état 2, repli sûr du mode dégradé L2, et
disparaît en release.
Aucune règle absolue 1 à 10 modifiée (vérifié identique à HEAD).
INVARIANTS BUILD / PACKAGING intact. Aucun code touché.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9624b6ec87
commit
f6a4be5560
295
AGENTS.md
295
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`.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user