799 lines
46 KiB
Markdown
799 lines
46 KiB
Markdown
# SPEC — ECS multi-palier
|
|
|
|
Version : 0.5.5
|
|
Dépôt : `etm-powersync-energy-plugin-etm` (experience-plugin ems)
|
|
Branche : `feature/beta-rulebased`
|
|
Contrats faisant autorité : `docs/OPTIMIZER_PROTOCOL.md`, `docs/SAFETY.md`, `AGENTS.md`
|
|
|
|
Ce document est **normatif** et **subordonné à `AGENTS.md`**. En cas de conflit,
|
|
`AGENTS.md` gagne et cette spec est corrigée.
|
|
|
|
Chaque exigence porte un identifiant stable. Un test porte l'identifiant de
|
|
l'exigence qu'il couvre. **DOIT** = obligatoire, **DEVRAIT** = recommandé avec
|
|
justification si écarté, **PEUT** = optionnel.
|
|
|
|
Un agent qui découvre une contradiction entre la spec et le code s'arrête et la
|
|
remonte. Il ne tranche pas seul.
|
|
|
|
> **0.4.0** — révision après audit du code réel. Les versions ≤ 0.3.0 nommaient
|
|
> `EcsRelayAdapter` (classe supprimée en `5100674`) et attribuaient le choix du
|
|
> palier au scheduler. Corrigé ci-dessous. Ne pas se fier à une copie antérieure.
|
|
>
|
|
> **0.4.1** — passe de vérification de la 0.4.0 contre le code. Quatre
|
|
> corrections : statut réel des binaires de test (§9), portée exacte du point de
|
|
> gouvernance `AGENTS.md` (§0), citation de règle dans ECS-306 (§3), et
|
|
> recouvrement ECS-411 ↔ ECS-412 porté en point ouvert (§13-2).
|
|
>
|
|
> **0.4.2** — étape 2 renommée « extraction du **noyau de calcul** » : le mot
|
|
> « domaine » entrait en collision avec `spec_loadmodel.md` LM-100, où il
|
|
> désigne la couche métier `Ecs`/`Hvac`/`Ev`/`SmartHome`. Seul changement.
|
|
>
|
|
> **0.4.3** — ECS-110 étendu : la validation ne doit pas reposer sur `Q_ASSERT`,
|
|
> absent du binaire release. Constat issu de la consolidation d'`AGENTS.md`.
|
|
>
|
|
> **0.5.0** — §13-1 et §13-2 **tranchés et consignés** ; l'étape 1 est débloquée.
|
|
> ECS-412 réduit à `m_lastSwitch` (lecture (b)) et redirigé vers sa cause racine.
|
|
>
|
|
> **0.5.1 — CORRECTION du câblage.** La 0.5.0 consignait 500/1000/**2000** ;
|
|
> c'était faux. Le câblage réel est **500/1000/1500**. Trois consignations
|
|
> s'inversent : ECS-302 **est** exercé et redevient préalable structurel de
|
|
> l'étape 3 ; **ECS-303 est supprimé** (le « point dur » était une conséquence
|
|
> d'ECS-302 non traité, pas une exigence d'hystérésis) ; 7 valeurs et plafond
|
|
> 3000 W, non 8 et 3500. Ne pas se fier à une copie 0.5.0.
|
|
>
|
|
> **0.5.2 — CORRECTION du câblage et DÉCOUPLAGE.** Le câblage réel est
|
|
> **500/1000/2000** ; la 0.5.1, qui l'avait corrigé en 500/1000/1500 sur la foi
|
|
> du simulateur, était fausse — le simulateur ne reflète pas l'installation.
|
|
> Quatrième révision de cette donnée. Trois consignations reviennent à l'état de
|
|
> la 0.5.0 : ECS-302 **non exercé** par l'installation de référence et redescendu
|
|
> dans l'ordre de l'étape 3 ; **ECS-303 rétabli** ; 8 valeurs, plafond 3500 W.
|
|
>
|
|
> Pour que cela ne se reproduise pas, les puissances ne vivent plus **que** dans
|
|
> le bloc §4.0 « Installation de référence ». Aucune exigence n'en contient. Une
|
|
> exigence dont le rang ou la portée en dépend le dit par renvoi.
|
|
|
|
---
|
|
|
|
## §0 — Domicile et découpage (tranché)
|
|
|
|
**ECS-001 — RÉSOLU, négatif, reconfirmé contre le `libnymea` du build.**
|
|
`/usr/include/nymea/integrations/integrationplugin.h` : la section `protected`
|
|
n'expose que `myThings()`, `hardwareManager()`, `pluginStorage()`,
|
|
`apiKeyStorage()`, `setMetaData()`. Pas de `thingManager()`. Un
|
|
`IntegrationPlugin` ne peut pas commander un Thing d'un autre plugin.
|
|
|
|
**ECS-002** — Il n'y a **ni integration-plugin, ni dépôt, ni ThingClass**. L'ECS
|
|
ne possède aucun matériel.
|
|
|
|
**ECS-003 — Découpage réel (rév. 3).** Les classes en vigueur :
|
|
|
|
| Composant | Fichier | Rôle |
|
|
|---|---|---|
|
|
| `RuleBasedScheduler` | `etm/scheduler/rulebasedscheduler.cpp` | Alloue le budget de surplus, cascade par `priority` ascendante, arrondit sur les paliers dérivés |
|
|
| `RelayRouter` | `etm/adapters/relayrouter.{h,cpp}` | Convertit des watts en palier, tient les verrous `minOn`/`minOff`, écrit les Things `powerswitch` |
|
|
| `LoadConfig`, `LoadConfigRelay` | `etm/types/loadconfig.{h,cpp}` | Description d'une charge |
|
|
| `LoadConfigStore` | `etm/config/loadconfigstore.{h,cpp}` | Persistance + RPC `Get/SetLoadConfig` |
|
|
| `EtmVariableLoadAdapter` | `etm/adapters/etmvariableloadadapter.{h,cpp}` | Charge continue (triac) |
|
|
|
|
**ECS-004 / ECS-005 — Frontière tenue.** Aucun identifiant de relais ne franchit
|
|
la frontière vers le scheduler : seuls des watts dérivés circulent. Cette
|
|
propriété est **acquise et non négociable** ; toute évolution la préserve.
|
|
|
|
> **Point de gouvernance à trancher hors spec — c'est le SCHÉMA d'`AGENTS.md`
|
|
> qui est périmé, pas la règle 2.**
|
|
>
|
|
> Portée exacte, vérifiée : la règle absolue 2 dit « Les LoadAdapters exécutent,
|
|
> ils ne décident pas […] **Aucune logique de répartition dedans.** » Le
|
|
> `RelayRouter` ne fait **aucune répartition** — il traduit une enveloppe en
|
|
> watts vers une combinaison, il ne s'attribue rien (ECS-007 tient). **Telle
|
|
> qu'elle est écrite, la règle 2 n'est pas violée.** Aucun agent ne doit conclure
|
|
> d'ici qu'il faut réécrire l'architecture.
|
|
>
|
|
> Ce qui contredit réellement le code, c'est le schéma « ARCHITECTURE CIBLE (non
|
|
> négociable) », `AGENTS.md:228-250`, qui affiche encore :
|
|
>
|
|
> ```
|
|
> EvAdapter EcsRelayAdapter SgReadyAdapter BatteryAdapter
|
|
> (setpoint, (stage 0/1/2) (state 1-4)
|
|
> ```
|
|
>
|
|
> Deux éléments morts d'un coup : la classe `EcsRelayAdapter` **et** le kind
|
|
> `Stage`, tous deux supprimés en `5100674`.
|
|
>
|
|
> **Décision attendue : redessiner ce schéma dans `AGENTS.md`** (`RelayRouter`,
|
|
> kind `Setpoint`, frontière watts↔relais), et non seulement « entériner une
|
|
> frontière ». Tant que ce n'est pas fait, le document normatif décrit des
|
|
> classes qui n'existent plus.
|
|
|
|
**ECS-007** — Aucune couche ECS NE DOIT décider quelle part du surplus lui
|
|
revient. Acquis (`rulebasedscheduler.cpp:97-121`).
|
|
|
|
**ECS-008** — L'ECS publie `available` et `energyRemaining` ; Héos les consomme.
|
|
L'ems exécute, Héos optimise.
|
|
|
|
---
|
|
|
|
## §1 — État des exigences (après audit)
|
|
|
|
Le code **n'est pas en production client** : ni sur `main` (`f4d5b20`), ni sur un
|
|
tag. Il vit sur `feature/beta-rulebased` et `landing-silo`, rattaché au banc
|
|
hems. Une modification de comportement ne casse donc aucune installation.
|
|
|
|
**Décision : ÉTENDRE, pas refondre.** La frontière watts↔relais est posée et
|
|
tenue, l'énumération des sous-ensembles est correcte et testée, la config est
|
|
externalisée. Le mélange avec nymea est superficiel : trois fonctions pures
|
|
(`m_levels`, `stageForPower`, `lockWindow`) noyées dans une classe qui fait
|
|
aussi l'I/O — une extraction d'environ 40 lignes, pas une reconstruction.
|
|
|
|
| Exigence | Objet | Statut |
|
|
|---|---|---|
|
|
| ECS-001 | Pas d'accès cross-thing | ✅ reconfirmé sur le libnymea du build |
|
|
| ECS-100 | Désignation par `ThingId` | ✅ `loadconfig.h:46` |
|
|
| ECS-101 | Cas à un seul étage | ✅ `relayrouter.cpp:49-64`, testé |
|
|
| ECS-102 | Configuration externalisée | ✅ `LoadConfigStore` + RPC — était marqué ouvert à tort |
|
|
| ECS-300 | Palier ≤ budget | ⚠️ vrai hors verrou, faux sous verrou → **ECS-306** |
|
|
| ECS-301 | Puissances quelconques, non cascadé | ✅ énumération des 2^N sous-ensembles, testé sur une topologie non cascadée |
|
|
| ECS-302 | Encodages équivalents | ❌ le second encodage est jeté (structure) — **non exercé par l'installation de référence**, cf. §4.0 |
|
|
| ECS-304 | Testable hors nymea | ⚠️ 3 fonctions pures extractibles, le reste couplé |
|
|
| ECS-400 | Temporisations non codées en dur | ✅ `minOnS`/`minOffS` de la config |
|
|
| ECS-402 | Coupure avant fermeture | ✅ `relayrouter.cpp:240-244`, testé |
|
|
| ECS-403 | Clamp lock-aware côté scheduler | ❌ `minStage`/`maxStage` **retirés** du contexte en rév. 2/3 |
|
|
| ECS-404 | Anti-oscillation | ✅ recrédit `currentPowerW`, `rulebasedscheduler.cpp:186` |
|
|
| ECS-110 | Validation de configuration | ⚠️ partielle |
|
|
| ECS-111 | Suppression d'un Thing référencé | ❌ non défini |
|
|
| ECS-305 | Compteur de commutations | ❌ absent |
|
|
| ECS-410 | Échec d'écriture | ❌ absent, `available` codé en dur |
|
|
| ECS-411 | Reprise du palier | ❌ absent |
|
|
| ECS-412 | Survie au `rebuild` | ❌ absent (nouveau) |
|
|
| §5 | Mesure par charge | ⚠️ demi-pas |
|
|
| §6 | Thermique | ❌ absent, et sans canal d'entrée |
|
|
|
|
**ECS-203 — CLOS.** Aucune ThingClass exposée, donc aucune interface nymea à
|
|
implémenter. L'unité interne reste le **watt**.
|
|
|
|
---
|
|
|
|
## §2 — Ordre de traitement
|
|
|
|
L'ordre est normatif : chaque étape conditionne la suivante.
|
|
|
|
**Étape 1 — corrections de comportement (ECS-306, ECS-412).** Ce sont des
|
|
défauts qui produisent un mauvais comportement sur du code qui tourne au banc,
|
|
pas des fonctionnalités manquantes. Ils passent devant tout le reste.
|
|
|
|
> **Tranché le 2026-08-08 (§13-2, lecture (b)) — l'étape 1 est OUVERTE.** Elle
|
|
> porte **trois** exigences : ECS-306, **ECS-411** (remonté ici) et ECS-412
|
|
> **réduit à `m_lastSwitch`**. `m_currentStage` se redéduit des Things une fois
|
|
> ECS-411 en place : il ne demande aucun code dédié.
|
|
|
|
**Étape 2 — extraction du noyau de calcul (type pur, sans `ThingManager`).**
|
|
Sortir `m_levels`, `stageForPower` et `lockWindow`. Débloque ECS-304 en vrai
|
|
unitaire et conditionne l'étape 3.
|
|
|
|
> Ne pas lire « domaine » ici. `specs/spec_loadmodel.md` LM-100 emploie ce mot
|
|
> dans son sens courant — la couche métier `Ecs`/`Hvac`/`Ev`/`SmartHome`, qui ne
|
|
> parle à aucun matériel. L'étape 2 ne sort **aucune** classe de ce genre : elle
|
|
> extrait le noyau de calcul **interne au mécanisme relais**. Rien à voir.
|
|
|
|
**Étape 3 — ECS-302 / ECS-305.** Changement de structure de `m_relayMapping` :
|
|
un palier doit pouvoir porter PLUSIEURS combinaisons.
|
|
|
|
> **ECS-305 passe devant ECS-302.** L'installation de référence (§4.0) n'exerce
|
|
> pas ECS-302 — aucun palier n'y a deux encodages — tandis que sa pondération
|
|
> binaire déséquilibre fortement l'usure, ce que seul ECS-305 rend mesurable.
|
|
> ECS-302 reste dans cette étape, mais il n'en est plus le préalable structurel.
|
|
>
|
|
> **Ne pas ouvrir l'étape 3 avant la confirmation physique du câblage** (§4.0,
|
|
> §13-3) : c'est elle qui décide de l'ordre interne de cette étape.
|
|
|
|
**Étape 4 — ECS-410** (échec d'écriture). ECS-411 a été **remonté en étape 1**
|
|
(§13-2). ECS-410 reste ici : il touche le même chemin `applyRelayStage` /
|
|
`available`, mais il demande d'attendre le résultat de `executeAction`, ce qui
|
|
est un changement de nature — pas une lecture d'état au démarrage.
|
|
|
|
**Étape 5 — ECS-110 (complément) et ECS-111.**
|
|
|
|
**Étape 6 — remontée d'`available` dans `LoadContext`**, avec la mise à jour de
|
|
`OPTIMIZER_PROTOCOL.md` qu'exige §9 dans le même lot. Préalable structurel au
|
|
thermique.
|
|
|
|
**Étape 7 — §6 thermique.**
|
|
|
|
---
|
|
|
|
## §3 — Corrections de comportement
|
|
|
|
**ECS-306 — Cohérence du budget avec le palier réellement appliqué.** Le budget
|
|
décrémenté par le scheduler DOIT correspondre au palier effectivement appliqué,
|
|
y compris lorsqu'un verrou empêche la descente.
|
|
|
|
Constat : `lockWindow` (`relayrouter.cpp:193-203`) remonte `minStage` à
|
|
`m_currentStage` tant que `minOn` n'est pas écoulé, et `qBound` (l. 155) force
|
|
alors un palier **au-dessus** du budget ; or le scheduler a déjà retranché le
|
|
palier plus bas qu'il avait choisi (`rulebasedscheduler.cpp:223`). Pendant toute
|
|
la fenêtre `minOn`, les charges de priorité suivante reçoivent un résidu
|
|
**surestimé** — donc l'installation soutire au réseau. Ce n'est pas une décision
|
|
d'allocation (ECS-007 tient), c'est un effet de bord sur l'allocation.
|
|
|
|
Règle visée : **`AGENTS.md` règle absolue 4** — « bornes par adaptateur
|
|
**écrêtent TOUTE sortie** de stratégie ». L'écrêtage existe bien (`qBound`,
|
|
`relayrouter.cpp:155`), mais son résultat n'est jamais renvoyé à l'arbitre :
|
|
c'est exactement le trou. *Ne pas invoquer ici la règle 1* — son mécanisme est
|
|
« deux décideurs sur le même surplus », et il n'y en a qu'un. Un agent qui suit
|
|
cette piste ira chercher un second décideur qui n'existe pas ; le défaut est un
|
|
décalage de comptabilité entre décision et exécution.
|
|
|
|
Le canal qui rendait le scheduler lock-aware **a existé** : `minStage`/`maxStage`
|
|
dans `LoadContextTelemetry`, retirés en rév. 2/3 (`surpluscontext.h:62-64`). La
|
|
correction DEVRAIT restaurer un signal de verrou vers le scheduler plutôt que se
|
|
contenter de remonter le palier appliqué après coup : la première corrige dans le
|
|
cycle, la seconde seulement au cycle suivant.
|
|
|
|
**ECS-412 — Survie des horodatages de verrou au `rebuild`.**
|
|
`rebuildLoadAdapters()` (`energyarbitrator.cpp:137-144`) détruit et reconstruit
|
|
les adaptateurs à chaque `SetLoadConfig`, remettant `m_currentStage` **et**
|
|
`m_lastSwitch` à zéro.
|
|
|
|
**Périmètre réduit à `m_lastSwitch`** (§13-2, lecture (b)) : `m_currentStage` se
|
|
redéduit de l'état réel des Things une fois ECS-411 en place, donc il ne demande
|
|
aucun code dédié ici. `m_lastSwitch` est l'irréductible — l'horodatage d'un
|
|
verrou n'existe nulle part dans le matériel — et c'est la part qui protège le
|
|
compresseur.
|
|
|
|
**Corriger la cause racine, pas le symptôme.** Ne PAS transporter `m_lastSwitch`
|
|
à travers la reconstruction : `rebuildLoadAdapters()` DOIT ne reconstruire **que
|
|
ce qui a changé**. Une charge dont le câblage et les verrous sont identiques ne
|
|
doit pas être détruite parce qu'une AUTRE charge a changé de priorité. Cela règle
|
|
`m_lastSwitch`, évite des réécritures de relais inutiles, et vaut pour toutes les
|
|
charges — y compris `EtmVariableLoadAdapter`.
|
|
|
|
**Démarrage à froid.** Après un redémarrage de `nymead`, `m_lastSwitch` est
|
|
irrécupérable. Le défaut sûr est alors d'**ARMER** le verrou — traiter la
|
|
dernière commutation comme venant d'avoir lieu — et non de le purger. Une boucle
|
|
de redémarrage court-circuiterait sinon la protection compresseur exactement
|
|
quand elle est la plus nécessaire. **L'implémentation naturelle fait l'inverse
|
|
(`QDateTime` nul = verrou inactif) : ce point DOIT être écrit explicitement dans
|
|
le code et couvert par un test.**
|
|
|
|
**Cet armement DOIT être TRANSITOIRE**, d'une durée égale au verrou configuré, et
|
|
jamais permanent. Il DOIT être posé **paresseusement**, au premier `now` reçu par
|
|
le chemin non-const (`applyAction()`), et non calculé dans la fenêtre de verrou :
|
|
un horodatage nul servant de sentinelle « écoulé = 0 » à chaque cycle produit un
|
|
**blocage circulaire** — une charge démarrant au palier 0 avec `minOffS > 0` ne
|
|
peut jamais s'enclencher, donc jamais commuter, donc jamais valider son
|
|
horodatage. Passer `now` au constructeur n'est PAS la réponse : cela changerait le
|
|
contrat d'`ILoadAdapter` pour un cas particulier.
|
|
|
|
**Symétrie exigée.** Les deux cas de démarrage à froid sont armés de la même
|
|
façon et expirent de la même façon :
|
|
|
|
| État initial des relais | Palier déduit (ECS-411) | Verrou armé |
|
|
|---|---|---|
|
|
| fermés | non nul | `minOn` |
|
|
| ouverts | 0 | `minOff` |
|
|
|
|
Le second cas protège d'un `nymead` qui redémarre juste après une ouverture : le
|
|
maintien est justifié, c'est sa **permanence** qui ne l'est pas.
|
|
|
|
Cette exigence porte son test : **`testEcsColdStartLockExpires`** — palier 0 au
|
|
départ, `minOffS > 0`, budget largement suffisant ; la charge reste éteinte
|
|
pendant `minOffS`, puis s'enclenche. Le défaut a été constaté **au banc** le
|
|
2026-08-09 (charge sonde figée à 0 W sous 4 kW de surplus disponible), alors
|
|
qu'aucun test ne combinait « palier 0 au départ » et « `minOffS > 0` ».
|
|
|
|
Il ne s'agit pas de confort : les verrous sont de la **protection matérielle**.
|
|
Avec un `minOn` de 300 à 600 s sur un ballon thermodynamique, un client qui
|
|
réordonne ses priorités depuis l'app réarme les verrous et peut faire
|
|
court-cycler son compresseur. Un test DOIT couvrir « `SetLoadConfig` pendant une
|
|
fenêtre de verrou active ».
|
|
|
|
> **Recouvrement avec ECS-411 — ordonnancement à arbitrer (§13-2).** Les deux
|
|
> champs à préserver n'ont pas le même statut. `m_currentStage` est
|
|
> **redéductible** : si ECS-411 est en place, un `RelayRouter` reconstruit
|
|
> retrouve son palier depuis les Things sans code dédié. `m_lastSwitch` ne l'est
|
|
> **pas** — l'horodatage d'un verrou n'existe nulle part dans le matériel, et
|
|
> c'est justement la part qui protège le compresseur. ECS-412 a donc un
|
|
> irréductible (`m_lastSwitch`) et une part que l'étape 4 rendrait gratuite
|
|
> (`m_currentStage`). Voir §13-2 pour l'arbitrage.
|
|
|
|
---
|
|
|
|
## §4 — Combinatoire (`RelayRouter`)
|
|
|
|
## §4.0 — Installation de référence
|
|
|
|
**Ce bloc est le SEUL endroit de la spec qui porte des puissances.** Aucune
|
|
exigence n'en contient. Une exigence dont le rang, la portée ou l'exercice
|
|
dépendent du câblage le dit par renvoi ici. Une correction de câblage ne doit
|
|
toucher que ce bloc.
|
|
|
|
| Relais | Puissance |
|
|
|---|---|
|
|
| R1 | 500 W |
|
|
| R2 | 1000 W |
|
|
| R3 | 2000 W |
|
|
|
|
Additif, pondération binaire. **8 valeurs distinctes**, de 0 à **3500 W**, au pas
|
|
de 500 W. **Aucune redondance** : un seul encodage par palier.
|
|
|
|
| W | Encodage |
|
|
|---|---|
|
|
| 0 | — |
|
|
| 500 | R1 |
|
|
| 1000 | R2 |
|
|
| 1500 | R1+R2 |
|
|
| 2000 | R3 |
|
|
| 2500 | R1+R3 |
|
|
| 3000 | R2+R3 |
|
|
| 3500 | R1+R2+R3 |
|
|
|
|
**Source : déclaration de l'utilisateur, 2026-08-08. PAS une mesure.** Cette
|
|
valeur a changé quatre fois. Une **vérification physique** — plaque signalétique
|
|
ou pince ampèremétrique — est **en attente**, et elle est un **préalable à
|
|
l'ouverture de l'étape 3**.
|
|
|
|
**Le simulateur du banc ne reflète pas cette installation.**
|
|
`etm-powersync-hems-sim`, `sim/sim_ecs_router.py:38`, est câblé
|
|
`{1: 500.0, 2: 1000.0, 3: 1500.0}`. Son alignement fait l'objet d'un lot séparé
|
|
sur ce dépôt. Corriger le banc pour qu'il corresponde au matériel réel n'est pas
|
|
« ajuster le banc pour faire passer un test » — c'en est l'inverse.
|
|
|
|
### Ce que cette installation exerce, et ce qu'elle n'exerce pas
|
|
|
|
| Point | Conséquence |
|
|
|---|---|
|
|
| Aucune redondance | **ECS-302 n'est pas exercé.** L'exigence reste valable — les puissances sont de la configuration, un autre chantier peut être câblé 1-2-3 — mais elle perd son caractère de préalable structurel et redescend dans l'ordre de l'étape 3. |
|
|
| Encodages uniques | La reprise d'état (ECS-411) ne peut pas diverger : toute somme lue correspond à un unique ensemble de relais. La correspondance exacte aboutit toujours. |
|
|
| Transition 1500 → 2000 W | `{R1+R2}` → `{R3}` : **les trois relais basculent d'un coup**, deux ouvertures et une fermeture. Seule transition du balayage dans ce cas. Voir **ECS-303**. |
|
|
| Usure par balayage complet (0 → 3500 W) | **R1 commute 7 fois, R2 3 fois, R3 une seule** — 11 commutations au total. Voir **ECS-305**. |
|
|
|
|
---
|
|
|
|
**ECS-302 — Encodages équivalents.** Un palier DOIT pouvoir porter **plusieurs**
|
|
combinaisons de relais, et la combinaison retenue DOIT être celle dont le coût de
|
|
transition depuis l'état courant est le plus faible : d'abord le nombre de relais
|
|
à basculer, puis le relais dont le compteur de commutations est le moins entamé.
|
|
|
|
Le second encodage n'est pas mal arbitré, il est **jeté au constructeur**.
|
|
`relayrouter.cpp:58` fait `if (!byPower.contains(sum)) byPower.insert(sum, set)`
|
|
— la première combinaison rencontrée gagne, c'est-à-dire le masque le plus bas.
|
|
`m_relayMapping` étant un `QList<QList<QString>>`, la structure **ne peut pas**
|
|
porter deux combinaisons. ECS-302 est donc un changement de structure, pas
|
|
l'ajout d'un critère.
|
|
|
|
> **Non exercé par l'installation de référence** (§4.0) : sa pondération binaire
|
|
> ne produit aucune redondance. L'exigence reste valable — les puissances sont
|
|
> des données de configuration — mais elle **n'est plus un préalable structurel**
|
|
> et redescend dans l'ordre de l'étape 3. Elle redeviendrait prioritaire sur une
|
|
> installation à encodages multiples.
|
|
|
|
**ECS-303 — Hystérésis élargie aux frontières de recombinaison.** Lorsque le
|
|
passage d'un palier au suivant impose de basculer plus de deux relais
|
|
simultanément, le seuil d'hystérésis de cette frontière DOIT être élargi par
|
|
rapport aux autres.
|
|
|
|
Justification : une recombinaison complète coûte le maximum de commutations et,
|
|
sous l'ordre coupure-avant-fermeture d'ECS-402, fait traverser un creux de
|
|
puissance à la charge. Une oscillation du surplus autour d'une telle frontière
|
|
est donc bien plus coûteuse qu'ailleurs, en usure comme en confort.
|
|
|
|
L'installation de référence (§4.0) présente exactement une frontière de ce type.
|
|
La largeur du seuil DOIT être établie par le relevé de commutations au banc, pas
|
|
choisie a priori.
|
|
|
|
**ECS-305 — Compteurs de commutations.** Chaque relais DOIT porter un compteur de
|
|
commutations, exposé en télémétrie.
|
|
|
|
Le relais de plus faible puissance est le bit de poids faible de la combinatoire
|
|
et s'use nettement plus vite que les autres ; le déséquilibre exact dépend du
|
|
câblage (§4.0). Le comptage par relais est **la seule façon de savoir quand le
|
|
premier contacteur arrivera en fin de vie** — la puissance commutée ne le dit pas.
|
|
|
|
\note Ces comptes dépendent aussi de l'encodage choisi : traiter ECS-302 les
|
|
modifie. Raison de plus pour instrumenter avant d'optimiser.
|
|
|
|
**ECS-307 — Grain des temporisations.** `minOn`/`minOff` sont aujourd'hui **par
|
|
charge** (un unique `m_lastSwitch`, `relayrouter.h:107`), ce qui satisfait
|
|
ECS-400. Le grain par relais qu'appelle ECS-305 n'existe pas. Décider en étape 3
|
|
s'il est nécessaire, ou si le compteur de commutations suffit à l'observation.
|
|
|
|
**ECS-308** — Le plafond `MaxRelays = 16` (`relayrouter.cpp:17`) tronque
|
|
silencieusement au-delà, avec un warning. Acceptable ; DOIT rester documenté.
|
|
|
|
---
|
|
|
|
## §5 — Robustesse d'exécution
|
|
|
|
**ECS-410 — Échec d'écriture.** `writeRelay` (`relayrouter.cpp:222-236`) jette le
|
|
`ThingActionInfo*` retourné par `executeAction` : aucune attente de `finished`,
|
|
aucun retour arrière, aucun arrêt total. `available` est codé en dur à `true`
|
|
(`relayrouter.cpp:91`). Un relais introuvable produit un warning, et
|
|
`m_currentStage` est mis à jour comme si tout avait réussi — **on annonce une
|
|
puissance non appliquée**, ce que ECS-410 interdit explicitement.
|
|
|
|
Comportement exigé : (1) attendre le résultat de `executeAction` ; (2) sur échec,
|
|
tenter le retour à l'état précédent ; (3) à défaut, commander l'arrêt total ;
|
|
(4) à défaut, `available = false` et cesser toute commande. Le canal existe déjà,
|
|
il est simplement ignoré.
|
|
|
|
> Sur l'installation de référence (§4.0), les encodages étant uniques, la
|
|
> correspondance exacte de l'ensemble de relais aboutit toujours et le modèle
|
|
> d'état ne peut pas diverger du physique. Sur un câblage à encodages multiples,
|
|
> une correspondance par puissance seule renverrait un palier dont l'encodage
|
|
> canonique diffère de l'ensemble réellement fermé, et la transition suivante
|
|
> calculerait son coupure-avant-fermeture contre un état faux. La reprise DOIT
|
|
> donc restituer l'ensemble de relais, pas seulement l'index de palier.
|
|
|
|
**ECS-411 — Reprise du palier.** `m_currentStage` vaut 0 à la construction
|
|
(`relayrouter.h:106`) et aucun état de relais n'est relu. Le palier courant DOIT
|
|
être déduit de l'état réel des Things.
|
|
|
|
**ECS-414 — Généralisation de l'échelle d'échec. FAIT (2026-08-09).** ECS-410 était
|
|
implémenté par le seul `RelayRouter`. `SgReadyAdapter` (`sgreadyadapter.cpp:212`) et
|
|
`EtmVariableLoadAdapter` (`etmvariableloadadapter.cpp:134`) jettent toujours le
|
|
`ThingActionInfo*` retourné par `executeAction` : ils gardent le défaut d'origine
|
|
— annoncer un état non appliqué.
|
|
|
|
L'échelle DOIT leur être étendue. `clearFault()` et `applySafeState()` sont déjà
|
|
déclarées sur ces adaptateurs, sans effet pour la première : le point d'accroche
|
|
existe, seul le suivi asynchrone manque.
|
|
|
|
**Le plancher est propre à chaque adaptateur** (même principe qu'ECS-413) :
|
|
consigne 0 W pour `EtmVariableLoadAdapter`, mais **état 2** pour `SgReadyAdapter`.
|
|
Ouvrir les deux contacts d'une PAC n'est pas une mise en sécurité, c'est une
|
|
commande — et selon l'encodage câblé, potentiellement le **blocage**.
|
|
|
|
**Le repli honore le même contrat d'atomicité que le chemin aller.** Un état
|
|
SG-Ready est porté par deux bits : si une écriture échoue, on se retrouve dans un
|
|
motif valide mais non voulu, et le retour depuis ce motif peut exiger de traverser
|
|
le blocage. `applyStateRelays()` prend donc un **ensemble de relais réellement
|
|
fermés**, et non un index d'état, afin que `transientHarm` ordonne le repli comme
|
|
il ordonne l'aller. Sans cela, la récupération serait plus dangereuse que la panne
|
|
qu'elle corrige.
|
|
|
|
**Les écritures de repli contournent `minStateHoldS`** — 900 s sur une PAC :
|
|
attendre un quart d'heure pour sortir d'un état non voulu n'est pas défendable.
|
|
|
|
\note Un contact **injoignable est supposé FERMÉ** pour la télémétrie (cohérence
|
|
ECS-411) **mais toujours COMMANDÉ** : ce qu'on ne peut pas vérifier doit être
|
|
écrit, sinon l'hypothèse conservatrice masquerait l'échec et la transition
|
|
paraîtrait réussie sans qu'aucune écriture n'ait été tentée.
|
|
|
|
> **Reste au lot de mise en configuration du `SgReadyAdapter`** : les paliers
|
|
> `{3: 1500, 4: 3000}` demeurent codés en dur (`energypluginnymea.cpp:66-76`).
|
|
> Ce lot porte sa propre précondition — retirer `Q_ASSERT(m_stateRelays.contains(2))`
|
|
> (`sgreadyadapter.cpp:36`) au profit d'un refus explicite, cf. ECS-110.
|
|
|
|
> **Le principe dégagé par ces deux exigences ne vit pas ici.** « Supposer
|
|
> conservateur pour annoncer, tenter systématiquement pour agir » vaut pour tout
|
|
> mécanisme, pas pour l'ECS : il est inscrit en **LM-104**
|
|
> (`specs/spec_loadmodel.md` §1). ECS-411 et ECS-414 en sont deux applications,
|
|
> pas la source.
|
|
|
|
**ECS-413 — Désactivation en état sûr.** Désactiver une charge (`enabled: false`)
|
|
ou la retirer de la configuration DOIT laisser son matériel dans l'**état sûr
|
|
défini par son adaptateur**, et non dans son dernier état commandé. L'état sûr est
|
|
appliqué **avant** la destruction de l'adaptateur ; si l'écriture échoue,
|
|
l'échelle d'ECS-410 s'applique.
|
|
|
|
Constaté au banc le 2026-08-09 : un `SetLoadConfig` posant `enabled: false` sur une
|
|
charge alors au palier 3500 W a détruit l'adaptateur en laissant **les trois relais
|
|
fermés**. Plus personne ne les commandait ; ils y seraient restés indéfiniment,
|
|
juste avant une intervention de câblage.
|
|
|
|
**L'état sûr est propre à chaque adaptateur** — une exigence formulée « tout
|
|
couper » serait FAUSSE :
|
|
|
|
| Adaptateur | État sûr |
|
|
|---|---|
|
|
| `RelayRouter` | tous relais **ouverts** |
|
|
| `EtmVariableLoadAdapter` | consigne **0 W** |
|
|
| `SgReadyAdapter` | **état 2** (normal, mains off) — **jamais** l'état 1 (blocage), cf. `docs/SAFETY.md` |
|
|
|
|
Couper une PAC en la bloquant serait une régression de sécurité, pas une mise en
|
|
sécurité.
|
|
|
|
**Mécanisme.** L'application passe par le **chemin d'action normal avec
|
|
`force = true`**, comme le mode dégradé L2 (`energyarbitrator.cpp:339-347`) :
|
|
c'est le mécanisme qui existe déjà pour contourner les verrous dans une situation
|
|
où l'arrêt prime. Rien de nouveau à inventer.
|
|
|
|
> **Cette exigence NE S'APPLIQUE PAS à l'arrêt du plugin ni à un redémarrage de
|
|
> `nymead`.** Là, l'état DOIT être conservé : c'est précisément ce qu'ECS-411 relit
|
|
> au démarrage, et couper l'eau chaude à chaque redémarrage de service serait une
|
|
> régression. La distinction est **intentionnelle** — la désactivation est un acte
|
|
> délibéré de l'opérateur, un redémarrage n'en est pas un.
|
|
|
|
**Test : `testEcsDisableLeavesSafeState`.** Charge à un palier non nul,
|
|
`SetLoadConfig` avec `enabled: false` ; vérifier que les relais sont **ouverts** et
|
|
que l'adaptateur n'est détruit qu'**ensuite**. Second cas obligatoire : un simple
|
|
`rebuild` **sans** désactivation NE DOIT PAS déclencher la mise en sécurité — sans
|
|
quoi ECS-412 serait annulé, chaque changement de rang coupant la charge.
|
|
|
|
---
|
|
|
|
## §6 — Configuration
|
|
|
|
**ECS-110 — Validation.** `LoadConfig::isValid()` (`loadconfig.cpp:80-127`)
|
|
refuse déjà `powerW ≤ 0`, `relays[]` vide, `thingId` vide, `minOnS`/`minOffS`
|
|
négatifs. DOIT en outre refuser : **deux étages sur le même Thing**, et un Thing
|
|
**absent ou n'exposant pas l'interface attendue**.
|
|
|
|
Note de conception : `isValid()` ne connaît pas le `ThingManager`, donc la
|
|
seconde vérification ne peut pas y vivre. Elle appartient à la construction de
|
|
l'adaptateur, pas au type de configuration.
|
|
|
|
**Validation effective en build release.** Aucune validation de configuration NE
|
|
DOIT reposer sur `Q_ASSERT`. Le paquet est construit par `dh --buildsystem=qmake6`
|
|
sans `CONFIG += debug` : qmake compile en release, `QT_NO_DEBUG` est défini, et
|
|
`Q_ASSERT` disparaît du binaire livré. Une vérification qui n'existe que chez le
|
|
développeur n'est pas une vérification — elle donne au lecteur du code
|
|
l'impression d'un filet qui n'est pas là chez le client.
|
|
|
|
Quatre `Q_ASSERT` d'invariant de configuration existent aujourd'hui dans `etm/` :
|
|
|
|
| Emplacement | Invariant gardé | Peut-il échouer aujourd'hui ? |
|
|
|---|---|---|
|
|
| `relayrouter.cpp:66` | `m_levels` non vide, `[0] == 0` | **Non** — le masque vide garantit la clé 0 par construction |
|
|
| `etmvariableloadadapter.cpp:34` | `powerLevels[0] == 0` | **Non** — déjà refusé par `isValid()` (`loadconfig.cpp:114-115`) |
|
|
| ~~`sgreadyadapter.cpp:35`~~ | `states` non vide | **SUPPRIMÉ (2026-08-09)** — remplacé par le refus `isValid()` |
|
|
| ~~`sgreadyadapter.cpp:36`~~ | état 2 présent = **repli sûr obligatoire** | **SUPPRIMÉ (2026-08-09)** — voir ci-dessous |
|
|
|
|
Aucun ne masque donc de défaut à ce jour : ECS-110 est bien tenu par
|
|
`LoadConfig::isValid()`, pas par ces assertions. **Le risque était prospectif et daté ; l'échéance est
|
|
tombée le 2026-08-09**, quand `SgReadyAdapter` est passé à la configuration. Les
|
|
deux assertions ont donc été retirées **avant** ce basculement, dans cet ordre,
|
|
et remplacées par deux gardes réels :
|
|
|
|
- `LoadConfig::isValid()` **refuse** toute configuration `sg-ready` dépourvue de
|
|
l'état 2, avec un message explicite. Une PAC inapte au repli sûr du mode
|
|
dégradé L2 n'est pas construite ;
|
|
- `SgReadyAdapter` conserve un drapeau `m_usable` : si on le construit malgré
|
|
tout hors du chemin de configuration, il **journalise en critique et refuse
|
|
toute commande** au lieu de disparaître sous `QT_NO_DEBUG`.
|
|
|
|
Exigence : toute vérification portant sur des données venant de la configuration
|
|
DOIT vivre dans `isValid()` ou dans la construction de l'adaptateur, avec un
|
|
chemin d'erreur explicite (refus + message). Les `Q_ASSERT` restants ne sont
|
|
admis que sur des invariants **structurels**, impossibles à violer depuis la
|
|
configuration — les deux premières lignes du tableau.
|
|
|
|
**ECS-110-b — Un Thing n'appartient qu'à UNE charge active.** Ajouté le 2026-08-09,
|
|
après vérification demandée : `LoadConfig::isValid()` s'arrête au bord d'une charge et
|
|
ne voyait donc **rien** du cas général. Le trou était réel et complet — deux charges de
|
|
configuration pouvaient revendiquer le même Thing (un relais déclaré à la fois dans la
|
|
charge ECS et dans un état SG-Ready), avec exactement les conséquences que la garde de
|
|
l'arbitre évite pour la PAC codée en dur : deux commandes contradictoires sur un organe,
|
|
et sa puissance comptée deux fois dans le budget (règle absolue 1). Rien ne l'empêchait,
|
|
et rien ne l'aurait signalé. Deux doublons **intra**-charge manquaient aussi : deux
|
|
étages de `relay-router` sur le même Thing (le routeur les fusionnait en silence, en
|
|
annonçant le double de la puissance réelle) et un relais répété dans un même état
|
|
SG-Ready.
|
|
|
|
La vérification d'ensemble ne peut pas vivre dans `isValid()` : elle est portée par
|
|
`LoadConfigStore::validateSet()`, appelée par `setConfigs()` (rejet total, rien persisté)
|
|
et au chargement (l'entrée en conflit est écartée, le reste du fichier survit — écarter
|
|
une charge ne commande rien, la charger commanderait faux).
|
|
|
|
Deux points de portée, tranchés :
|
|
|
|
- **Seules les charges `enabled` sont confrontées.** Une charge désactivée ne construit
|
|
aucun adaptateur ; l'interdire empêcherait de préparer une configuration de
|
|
remplacement. Activer repasse par `setConfigs()`, donc par la vérification ;
|
|
- **un relais présent dans plusieurs états SG-Ready reste légitime** — l'état 4 est
|
|
précisément l'union des états 1 et 3. Seule la répétition dans un **même** état est
|
|
refusée.
|
|
|
|
Les identifiants sont normalisés via `QUuid` : la configuration mélange `{uuid}` et
|
|
`uuid`, une comparaison textuelle aurait laissé passer le conflit. Les identifiants de
|
|
charge en double sont refusés par la même passe — la seconde entrée était jusqu'ici
|
|
perdue en silence à la construction.
|
|
|
|
**ECS-111 — Suppression d'un Thing référencé.** Comportement non défini
|
|
aujourd'hui : `findConfiguredThing` renvoie `nullptr`, `writeRelay` logue et
|
|
continue, `telemetry()` saute le relais donc `metered = false` et l'on retombe
|
|
sur le nominal commandé — **on publie la puissance d'un relais disparu**. À
|
|
définir et tester.
|
|
|
|
---
|
|
|
|
## §7 — Mesure par charge
|
|
|
|
`telemetry()` (`relayrouter.cpp:104-114`) lit déjà `currentPower` sur les Things
|
|
relais : c'est un demi-pas vers ECS-500.
|
|
|
|
**ECS-500** — Calibrage par mesure (enclencher chaque étage seul, mesurer,
|
|
écrire la puissance réelle). Justification : P = U²/R, un étage annoncé à 1000 W
|
|
sous 230 V délivre environ 1090 W sous 240 V.
|
|
|
|
**ECS-501** — Détecter un étage commandé dont la puissance mesurée est quasi
|
|
nulle, en distinguant le thermostat mécanique ouvert (tous les étages chutent
|
|
ensemble) d'un étage en défaut (un seul chute).
|
|
|
|
**ECS-502** — Un étage en défaut DOIT être retiré des paliers disponibles.
|
|
|
|
**ECS-503** — Sans mesure, le comportement actuel est conservé tel quel.
|
|
|
|
**ECS-504** — La mesure par charge NE DOIT PAS être réinjectée dans le calcul du
|
|
surplus (`AGENTS.md` règle 8, pas de boucle de feedback). Elle sert au diagnostic
|
|
et au recrédit anti-clignotement déjà en place.
|
|
|
|
> **Piège de mise en service, indépendant de toute configuration.** Le compteur
|
|
> d'une charge NE DOIT JAMAIS être désigné comme **rootmeter** dans nymea. Aucun
|
|
> champ de `LoadConfig` n'est en cause : il suffit d'une désignation dans
|
|
> l'interface. L'arbitre lit le rootmeter par `internalRootMeter()`
|
|
> (`energyarbitrator.cpp:229`) et en tire `meter.importW`/`exportW` — la
|
|
> consommation de la charge entrerait donc **dans le bilan de surplus lui-même**,
|
|
> et le budget serait faussé pour toutes les charges. Le compteur d'une charge est
|
|
> un instrument de diagnostic ; le rootmeter mesure le point de livraison.
|
|
|
|
---
|
|
|
|
## §8 — Thermique
|
|
|
|
Aucune température ni `energyRemaining` dans le dépôt. **Et il manque le canal
|
|
d'entrée** : `available` vit dans `LoadTelemetry` (`iloadadapter.h:15`) mais
|
|
l'arbitre ne le recopie pas dans `LoadContext`. Même avec une sonde posant
|
|
`available = false`, le scheduler ne le verrait pas. C'est un préalable
|
|
structurel, indépendant de tout le thermique (étape 6).
|
|
|
|
**ECS-600** — La sonde est optionnelle ; sans elle, le comportement actuel est
|
|
conservé intégralement.
|
|
|
|
**ECS-601** — Avec sonde, `available` passe à faux quand la consigne est
|
|
atteinte, afin que le waterfall libère le budget. C'est le **seul** point
|
|
d'entrée du thermique dans l'arbitrage : la charge se retire, elle ne négocie pas.
|
|
|
|
**ECS-602** — `energyRemaining` publié en kWh (`m · c · ΔT`).
|
|
|
|
**ECS-603** — Cycle anti-légionelle périodique, indépendant du surplus. Il
|
|
consomme du réseau : il DOIT passer par une `LoadAction` explicite avec son
|
|
`decisionReason`, jamais par un contournement du budget.
|
|
|
|
**ECS-604** — La sonde n'est jamais la seule limite haute. Le thermostat
|
|
mécanique reste **L0**. Perte de sonde → mode sans sonde, pas arrêt d'urgence.
|
|
|
|
**ECS-605** — Hystérésis paramétrable par charge, jamais codée en dur.
|
|
|
|
---
|
|
|
|
## §9 — Traçabilité (exigences ouvertes)
|
|
|
|
| Exigence | Type | Test |
|
|
|---|---|---|
|
|
| ECS-306 | simulation | `testEcsBudgetUnderLock` |
|
|
| ECS-412 | simulation | `testEcsRebuildPreservesLock` + **`testEcsColdStartLockExpires`** (armement à froid transitoire) |
|
|
| ECS-303 | banc | relevé de commutations à la frontière de recombinaison (§4.0) — **mesurer avant de coder** |
|
|
| ECS-305 | unitaire | `testEcsSwitchCount` — compteur par relais |
|
|
| ECS-302 | unitaire | `testEcsSwitchCost` — non exercé par l'installation de référence ; à tester sur un câblage à encodages multiples |
|
|
| ECS-304 | unitaire | `testEcsLevelsPure` |
|
|
| ECS-410 | simulation | `testEcsPartialFailure` — cible, retour arrière, arrêt total, défaut collant |
|
|
| ECS-411 | simulation | `testEcsRestartRecovery` |
|
|
| ECS-413 | simulation | `testEcsDisableLeavesSafeState` — + cas négatif : un rebuild sans désactivation ne coupe pas |
|
|
| ECS-414 | simulation | `testSgReadyPartialFailure` — plancher = état 2, atomicité du repli, contact injoignable |
|
|
| LM-302 (frontière RPC) | simulation | `testLoadConfigRpc` §3bis — va-et-vient Get → Set réinjecté tel quel (vérifié : le test échoue sans le correctif) |
|
|
| ECS-110-b | simulation | `testThingOwnershipIsExclusive` — conflit entre charges (3 mécanismes), formes d'uuid, id dupliqué, doublons intra-charge, tolérance d'une charge désactivée |
|
|
| ECS-110 (SG-Ready) / LM-300 | simulation | `testSgReadyFromConfig` — refus sans état 2, refus de charges utiles mélangées, round-trip, pilotage, désactivation → état 2 |
|
|
| ECS-110, ECS-111 | unitaire | `testEcsConfigValidation` — DOIT s'exécuter aussi en build release (`QT_NO_DEBUG`), sinon il ne prouve rien du binaire livré |
|
|
| ECS-501, ECS-502 | simulation | `testEcsStageFault` |
|
|
| ECS-601, ECS-602 | simulation | `testEcsTemperatureTarget` |
|
|
| ECS-603 | simulation | `testEcsLegionella` |
|
|
| ECS-600, ECS-604 | simulation | `testEcsNoSensorFallback` |
|
|
|
|
Acquis à ne pas refaire : `testEcsRelayTopologies`, `testLoadConfigRelayRouter`,
|
|
`testLoadConfigBuildsAdapters`, `testLoadConfigRpc`, `testEcsSurplusPV`,
|
|
`testMeterSilentFallback`.
|
|
|
|
> Des binaires de test datant du 8 juin — antérieurs au `RelayRouter` — traînent
|
|
> dans l'arbre de travail. **Rebuild systématique**, jamais de conclusion tirée
|
|
> d'un binaire trouvé sur place.
|
|
>
|
|
> Ils ne sont **pas versionnés** : `git ls-files tests/auto/simulation/nymea-energy-simulation`
|
|
> renvoie vide. Ce sont des reliquats de build que `.gitignore` ne couvre pas,
|
|
> d'où leur apparition permanente en `??`. Même cas : `qrc_*.cpp`, `moc_*`,
|
|
> `target_wrapper.sh`, et les `plugininfo.h` / `extern-plugininfo.h` générés du
|
|
> mock. Le remède est donc de **les ignorer ou les supprimer**, pas de « ne pas
|
|
> s'y fier ». Hygiène, hors périmètre ECS.
|
|
|
|
**Définition de fait** : celle d'`AGENTS.md`.
|
|
|
|
**Critère d'acceptation banc** : profil de surplus réel sur 24 h avec relevé du
|
|
nombre de commutations **par relais** (ECS-305 le rend enfin mesurable).
|
|
|
|
---
|
|
|
|
## §10 — Hors périmètre
|
|
|
|
Allocation du surplus, prévision et plan journalier (Héos), contacteur heures
|
|
creuses, transport matériel (Waveshare D8, session dédiée), PAC hors SG-Ready.
|
|
|
|
Signalé au passage, hors ECS : les `ThingId` K1/K2 et les paliers
|
|
`{3:1500, 4:3000}` de la PAC du banc sont codés en dur dans
|
|
`energypluginnymea.cpp:68-74`. À traiter avec la couche config, pas ici.
|
|
|
|
---
|
|
|
|
## §11 — Dette contractuelle à ne pas aggraver
|
|
|
|
`OPTIMIZER_PROTOCOL.md` ne reflète déjà pas `LoadAction.force`, les fenêtres de
|
|
verrou ni `degradedMode`. ECS-306 et l'étape 6 modifient le contexte. **Toute
|
|
exigence touchant au contexte ou aux actions DOIT embarquer sa mise à jour du
|
|
protocole dans le même lot.**
|
|
|
|
---
|
|
|
|
## §12 — Journal des décisions
|
|
|
|
| Date | Décision |
|
|
|---|---|
|
|
| 2026-08-06 | ECS multi-palier attaqué en premier ; ECS-simple = son cas à 1 palier |
|
|
| 2026-08-07 | Étages déclarés par référence de Thing + puissance par sortie |
|
|
| 2026-08-07 | ECS relais et ECS via PAC séparés ; les PAC reportées |
|
|
| 2026-08-08 | ECS-001 négatif → pas d'integration-plugin |
|
|
| 2026-08-08 | ECS-203 clos : unité interne = watt |
|
|
| 2026-08-08 | Audit : `RelayRouter` (et non `EcsRelayAdapter`), code hors production → **étendre** |
|
|
| 2026-08-08 | ECS-306 et ECS-412 créés et placés en étape 1 |
|
|
| 2026-08-08 | `ioConnections()` écarté formellement |
|
|
| 2026-08-08 | Vérification 0.4.1 : règle 2 non violée — c'est le schéma d'`AGENTS.md` qui est périmé |
|
|
| 2026-08-08 | ECS-306 rattaché à la règle 4 (écrêtage non remonté), pas à la règle 1 |
|
|
| 2026-08-08 | Recouvrement ECS-411 ↔ ECS-412 relevé → §13-2, bloque l'ouverture de l'étape 1 |
|
|
| 2026-08-08 | Étape 2 : « type domaine » → « noyau de calcul » (collision avec `spec_loadmodel.md` LM-100) |
|
|
| 2026-08-08 | ECS-110 : validation interdite de reposer sur `Q_ASSERT` (absent en release) |
|
|
| 2026-08-08 | **§13-1 CLOS** — schéma « ARCHITECTURE CIBLE » d'`AGENTS.md` redessiné (`f6a4be5`) |
|
|
| 2026-08-08 | **§13-2 CLOS, lecture (b)** — ECS-412 réduit à `m_lastSwitch` + cause racine (rebuild incrémental) ; ECS-411 remonté en étape 1 |
|
|
| 2026-08-08 | **Étape 1 OUVERTE** — trois exigences : ECS-306, ECS-411, ECS-412 |
|
|
| 2026-08-08 | ECS-412 : au démarrage à froid, le verrou est **ARMÉ** (défaut sûr), jamais purgé |
|
|
| 2026-08-09 | ECS-412 précisé : cet armement est **TRANSITOIRE** et posé paresseusement au premier `now`. Défaut de blocage circulaire constaté **au banc**, corrigé, couvert par `testEcsColdStartLockExpires` |
|
|
| 2026-08-10 | **Va-et-vient RPC cassé par LM-300** — `GetLoadConfig` sérialise via le méta-objet, donc un `sgReady` VIDE sur toute charge non SG-Ready ; le schéma SET exigeait `states` dès la clé présente, et nymea rejetait avant le handler. Constaté au banc à la première tentative réelle de `SetLoadConfig` : aucun test ne rejouait le geste de l'app (lire, modifier, réécrire). Corrigé en `o:states` — c'est le mur documenté en LM-302, la validation réelle restant dans `isValid()` |
|
|
| 2026-08-09 | **ECS-110-b créé et FAIT** — un Thing n'appartient qu'à une charge active. Trou trouvé en répondant à la question « le cas général est-il couvert ? » : il ne l'était pas du tout. `LoadConfigStore::validateSet()` ; doublons intra-charge également refusés |
|
|
| 2026-08-09 | **SG-Ready configurable (LM-300)** — union discriminée par mécanisme ; les deux `Q_ASSERT` de `sgreadyadapter.cpp` retirés au profit d'un refus `isValid()` + drapeau `m_usable` (ECS-110 tenu hors `Q_ASSERT`) ; principe général porté en LM-104, hors ECS |
|
|
| 2026-08-09 | **ECS-414 FAIT** — échelle généralisée à SgReady et EtmVariableLoad ; plancher = état 2 pour la PAC ; repli soumis au même `transientHarm` que l'aller ; contact injoignable supposé fermé mais toujours commandé |
|
|
| 2026-08-09 | **ECS-414 créé** — généralisation d'ECS-410 à SgReady et EtmVariableLoad, rattachée au lot SG-Ready en configuration |
|
|
| 2026-08-09 | **ECS-413 créé** — `enabled: false` laissait les relais fermés (constat de banc, juste avant une intervention de câblage). État sûr propre à chaque adaptateur ; ne s'applique PAS à un redémarrage de `nymead`, qu'ECS-411 couvre |
|
|
| 2026-08-08 | ~~Câblage 500/1000/2000~~ — **donnée fausse**, corrigée le jour même (cf. ligne suivante) |
|
|
| 2026-08-08 | ~~Câblage 500/1000/1500 (source simulateur)~~ — **donnée fausse** : le simulateur ne reflète pas l'installation |
|
|
| 2026-08-08 | ~~ECS-302 exercé~~ — découlait de la donnée fausse |
|
|
| 2026-08-08 | ~~ECS-303 supprimé~~ — découlait de la donnée fausse |
|
|
| 2026-08-08 | **Câblage 500/1000/2000** — 8 valeurs, plafond 3500 W, aucune redondance. Source : déclaration utilisateur, **non mesurée**. Quatrième révision de cette donnée ; vérification physique en attente, préalable à l'étape 3 |
|
|
| 2026-08-08 | **Découplage** : les puissances ne vivent plus que dans §4.0. Aucune exigence n'en contient |
|
|
| 2026-08-08 | **ECS-302 non exercé** par l'installation de référence — n'est plus préalable structurel ; ECS-305 passe devant dans l'étape 3 |
|
|
| 2026-08-08 | **ECS-303 rétabli** et reformulé sans puissances : hystérésis élargie aux frontières imposant une recombinaison de plus de deux relais |
|
|
| 2026-08-08 | Alignement du simulateur du banc sur 500/1000/2000 — lot séparé sur `etm-powersync-hems-sim` |
|
|
| 2026-08-08 | **Configuration de `.75` réglée pour ÉMULER l'installation de référence** — 3 relais GPIO déclarés 500/1000/2000, via `SetLoadConfig`. `.75` est un banc qui émule §4.0, il ne l'incarne pas : la vérification physique en attente porte sur l'**installation cliente**, pas sur les broches de la box de test |
|
|
| 2026-08-08 | Câblage du banc **déjà documenté** dans `docs/TEST_TERRAIN.md:24-26` (R500/R1000/R2000, broches BCM 5/6/13) — la configuration à 600/1200 était le reliquat, contredit par la documentation du banc lui-même |
|
|
|
|
---
|
|
|
|
## §13 — Points ouverts
|
|
|
|
1. **ECS-307** — grain des temporisations par relais : nécessaire, ou le compteur
|
|
de commutations suffit-il ? Le câblage binaire rend la question plus concrète
|
|
le câblage de référence déséquilibre fortement l'usure (§4.0), ce qui rend
|
|
la question plus concrète sans la trancher.
|
|
2. **ECS-605** — asymétrie de l'hystérésis montée/descente, à régler à l'usage.
|
|
La frontière de recombinaison relève désormais d'**ECS-303**.
|
|
3. **Câblage à confirmer physiquement** — la valeur de §4.0 provient d'une
|
|
déclaration, pas d'une mesure, et a changé quatre fois. Préalable à
|
|
l'ouverture de l'étape 3.
|
|
4. **Conflit d'écriture** — `connectIO()` est le mécanisme natif si l'on veut un
|
|
jour exposer un relais ECS au pilotage manuel de l'utilisateur. Ce serait
|
|
alors un conflit d'écriture à arbitrer, pas une aide.
|
|
|
|
**CLOS le 2026-08-08 :**
|
|
|
|
- **§13-1 gouvernance** — le schéma « ARCHITECTURE CIBLE » d'`AGENTS.md` a été
|
|
redessiné (lot de consolidation douze zones, commit `f6a4be5`) : `RelayRouter`
|
|
et `EtmVariableLoadAdapter` y figurent, le kind `Stage` en est retiré,
|
|
`SocketScheduler` et `BatteryAdapter` sont marqués non écrits.
|
|
- **§13-2 ordonnancement** — **lecture (b) retenue**. ECS-412 est réduit à
|
|
`m_lastSwitch` et redirigé vers sa cause racine (rebuild incrémental) ;
|
|
ECS-411 remonte en étape 1. L'étape 1 porte donc trois exigences.
|
|
`testEcsRebuildPreservesLock` couvre obligatoirement « `SetLoadConfig` pendant
|
|
une fenêtre de verrou active ».
|
|
|
|
**CLOS par l'audit** : le waterfall ne choisit pas entre encodages équivalents
|
|
(il n'en voit qu'un) ; `ioConnections()`/`connectIO()` est un lien 1↔1
|
|
état-à-état avec pour seule transformation un booléen `inverted` — il ne peut
|
|
exprimer ni le N→1, ni l'arithmétique, ni l'ordre de commutation, ni les
|
|
temporisations.
|