LM-1209-e. Le provisionnement vivait dans la branche « l'adaptateur vient d'être créé » de syncAdapters(). Or l'adaptateur est construit depuis le THING : il survit à la suppression de l'entrée de configuration. Une entrée supprimée depuis l'app n'était donc jamais recréée — mesuré sur .75, rien en 140 s. Le trou de 3g-2 rouvert par le côté client : la borne restait ARBITRÉE mais ABSENTE de GetLoadConfig, à un rang de queue non réglable. Elle consommait du budget sans que personne puisse la classer, et rien ne le disait. La condition devient un ÉTAT et non un ÉVÉNEMENT — « cette borne n'a pas d'entrée » au lieu de « l'adaptateur vient de naître ». C'est la leçon qui dépasse le défaut : un rattrapage dont la condition est un événement ne rattrape rien, puisqu'il ne se rejoue pas. Même distinction qu'entre ECS-411, qui relit l'état réel des relais, et l'hypothèse de départ qu'il a remplacée. L'entrée recréée revient en rankOrigin "auto" — personne ne l'a classée, son rang est de nouveau un défaut — et le rang posé par l'installateur est PERDU avec l'entrée. Supprimer n'est donc pas un moyen de retirer une borne de l'arbitrage : elle revient, déclassée, en queue et avec son badge. C'est exactement pourquoi la marque doit se voir. enabled: false reste la voie et conserve le rang. Le refus de provisionnement n'est annoncé qu'une fois par borne (m_provisionRefuse) : le rattrapage étant cyclique, le répéter produirait un avertissement par minute et pour toujours — la règle est déjà posée pour les charges sans décision dans buildTelemetry(). Le drapeau tombe au premier succès. testDeletedEvChargerConfigComesBack couvre les deux moitiés : le rattrapage, et le cas négatif — il ne se rejoue PAS quand l'entrée existe, sans quoi il réécrirait la configuration à chaque cycle, donc un changed(), donc un rebuild d'adaptateurs, donc des verrous réarmés en boucle (ECS-412). Vérifié échouant avec l'ancien comportement. Le brief §6-C, qui promettait à l'app l'inverse de ce qui se passait sur le seul geste client qui détruit de la configuration, est corrigé — et dit désormais ce que la suppression coûte vraiment : le rang. Passe aussi LM-1209-d (grouper à l'affichage, jamais à l'écriture) et l'avertissement de sonde en tête d'INTERFACE.md. amd64 0/0. simulation 32/32, charging 16/16, loadmodel 20/20, spotmarket 7/7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
1714 lines
104 KiB
Markdown
1714 lines
104 KiB
Markdown
# SPEC — Modèle de charges (domaine × mécanisme)
|
||
|
||
Version : 0.2.3
|
||
Dépôt : `etm-powersync-energy-plugin-etm`
|
||
Statut : **intention de conception figée.** §3 (schéma persisté) est **en cours
|
||
d'implémentation** depuis le 2026-08-09 : `relay-router`, `etmvariableload` et
|
||
`sg-ready` sont trois charges utiles discriminées par `adapter`. Le reste du
|
||
document reste une intention.
|
||
|
||
> **0.2.3** — LM-1203-b : sous la réserve de stockage, le budget est annulé et non réduit ;
|
||
> avec son corollaire — une réserve réglée haut gèle toute l'installation sans rien signaler —
|
||
> et les deux exigences qui l'accompagnent (motif dédié, défaut d'usine ramené de 0,9 à 0,2).
|
||
|
||
> **0.2.2** — LM-1204-b : le scénario d'acceptation de 3g, avec son piège de simulateur et le
|
||
> motif qui le déjoue. Ajouté quand le banc a reçu une seconde borne — et **il reste
|
||
> inexerçable tant que 3g n'est pas fait**, ce qui est précisément ce qu'il mesure.
|
||
|
||
> **0.2.1** — spec relue et validée (Patrick, 2026-08-26). Deux décisions inscrites :
|
||
> l'extension du délestage au waterfall devient une **précondition de livraison** du §10 —
|
||
> écrite comme condition et non comme conseil — et la cible de confort est tranchée
|
||
> (LM-1008) : saisie en degrés, frontière de l'arbitre en énergie, conversion à la couche
|
||
> domaine — et aucune conversion nécessaire aujourd'hui, un comparateur suffit. Ordre
|
||
> d'implémentation retenu : §11, puis §12, puis §10.
|
||
|
||
> **0.2.0** — trois manques du modèle spécifiés, **avant** implémentation : le besoin à deux
|
||
> niveaux (§10, LM-1000+), le compteur et la sonde par charge (§11, LM-1100+), le plancher de
|
||
> modulation (§12, LM-1200+). Écrits maintenant parce qu'écrits tard ils se heurteraient à des
|
||
> structures déjà figées. **Aucun code ne suit ce document** — l'ordre d'implémentation
|
||
> recommandé est en §13, et il n'est pas celui dans lequel les trois ont été demandés.
|
||
|
||
> **0.1.4** — LM-201 et LM-202 ajoutés (§2). **LM-201 est la règle qui manquait** : une
|
||
> charge existe quand il y a un **canal de commande distinct pour un service**. Un canal, une
|
||
> charge. Elle repose la question du domaine, que 0.1.3 posait mal — le domaine ne décrit pas la
|
||
> **machine**, il décrit **ce qu'on arbitre**. Elle explique du même coup la contrainte
|
||
> d'exclusivité de `validateSet()`, qui cesse d'être une limite technique subie.
|
||
> **LM-202** : les thermostats et les zones ne sont pas des charges.
|
||
|
||
> **0.1.3** — LM-200 ajouté (§2) : toutes les cases de la matrice ne se valent pas.
|
||
> Certaines sont *non prévues*, d'autres sont **interdites par la physique** — le cas de la
|
||
> PAC, qui ne se pilote qu'en `SgReady` ou en communicant. Précise LM-101, qui était trop
|
||
> permissif lu seul. Aucune décision de conception renversée ; une contrainte qui existait
|
||
> dans le matériel et pas dans le document.
|
||
|
||
> **0.1.2** — LM-104 ajouté (§1) : « supposer conservateur pour annoncer, tenter
|
||
> systématiquement pour agir ». Principe dégagé des tests d'ECS-411 et ECS-414,
|
||
> inscrit ici parce qu'il vaut pour tout mécanisme et non pour l'ECS seul.
|
||
> §3 : le mécanisme `sg-ready` a sa charge utile et cesse d'être une intention.
|
||
|
||
> **0.1.1** — passe de vérification contre le code. Quatre précisions :
|
||
> préséance des verrous (LM-303), portée de l'union discriminée (LM-302),
|
||
> citation de règle (LM-102), `EvAdapter` inscrit au renommage (LM-601).
|
||
> Aucune décision de conception modifiée.
|
||
|
||
> **Ce document ne déclenche aucun travail.** L'ordre de `specs/spec_ecs.md` §2
|
||
> reste seul en vigueur, et l'étape 1 reste bloquée par ses arbitrages §13-1 et
|
||
> §13-2. Une seule chose ici est urgente : la **forme du schéma persisté** (§3),
|
||
> parce que c'est la seule partie coûteuse à rattraper une fois que le banc et la
|
||
> beta auront écrit des données. Le reste s'implémente au fil des besoins.
|
||
|
||
---
|
||
|
||
## §1 — Deux couches
|
||
|
||
**LM-100** — Le modèle de charges se découpe en deux couches indépendantes :
|
||
|
||
- **Couche domaine** — `Ecs`, `Hvac`, `Ev`, `SmartHome`. Détient le modèle de la
|
||
charge (état thermique, échéance de départ, cycle), en déduit la demande et
|
||
`available`. **Ne parle à aucun matériel.**
|
||
- **Couche mécanisme** — `Relay`, `Variable`, `SgReady`, `ModbusSetpoint`.
|
||
Traduit une enveloppe en watts vers le matériel et tient les verrous.
|
||
**Écrite une seule fois, partagée par tous les domaines.**
|
||
|
||
**LM-101** — Une charge configurée choisit **un domaine et un mécanisme**. Le
|
||
domaine ne détermine pas le mécanisme et réciproquement — mais il en **exclut**
|
||
certains, cf. **LM-200**. L'indépendance des deux couches porte sur le *couplage
|
||
de conception*, pas sur la liberté de combiner : elle dit qu'aucune des deux
|
||
n'a besoin de connaître l'autre pour être écrite, jamais que toute paire est
|
||
réalisable.
|
||
|
||
**LM-102** — Aucune des deux couches ne répartit de budget. La couche domaine se
|
||
retire de l'allocation via `available` ; elle ne la négocie pas. Règles absolues
|
||
1 et 2 d'`AGENTS.md` inchangées.
|
||
|
||
**LM-104 — Supposer conservateur pour annoncer, tenter systématiquement pour agir.**
|
||
Un adaptateur qui ne peut pas LIRE l'état d'un organe DOIT le supposer dans l'état
|
||
le plus consommateur (contact fermé, palier engagé) quand il **annonce** — dans
|
||
`telemetry()`, `toLoadContext()` ou la déduction de son état courant au démarrage.
|
||
Le même adaptateur DOIT néanmoins **émettre l'écriture** vers cet organe quand il
|
||
agit, sans jamais la court-circuiter au motif que l'état supposé coïncide déjà avec
|
||
la cible.
|
||
|
||
Les deux moitiés sont indissociables, et c'est l'omission de la seconde qui est
|
||
piégeuse : supposer « fermé » puis en déduire « donc rien à écrire » transforme une
|
||
hypothèse de prudence en **masquage de panne** — l'organe reste dans son état réel,
|
||
inconnu, et aucun verdict d'échec ne se déclenche. La prudence porte sur ce qu'on
|
||
DIT de l'installation, jamais sur ce qu'on lui ENVOIE.
|
||
|
||
Portée : tout mécanisme, présent et futur. `Relay` (déduction de palier au
|
||
démarrage), `SgReady` (relevé du motif de contacts réellement fermés), et par
|
||
construction `ModbusSetpoint` dès qu'un registre devient illisible. Origine :
|
||
deux défauts symétriques trouvés par les tests d'ECS-411 puis d'ECS-414 —
|
||
d'abord l'hypothèse inverse (« injoignable donc ouvert »), puis son corollaire
|
||
(« supposé fermé donc jamais écrit »).
|
||
|
||
**Justification.** Un nommage par domaine seul (`EcsAdapter`, `HvacAdapter`)
|
||
obligerait à écrire l'encodage SG-Ready deux fois (ECS et HVAC), la combinatoire
|
||
de relais deux fois (ECS et EV en prise commandée), et le transport Modbus deux
|
||
fois (HVAC et EV). Un nommage par mécanisme seul perdrait les comportements
|
||
propres au domaine, qui sont réels : stockage différable pour l'ECS, confort
|
||
immédiat pour le HVAC, échéance de départ pour l'EV, cycle non modulable pour le
|
||
SmartHome.
|
||
|
||
---
|
||
|
||
## §2 — Matrice
|
||
|
||
| Domaine | Mécanismes attendus |
|
||
|---|---|
|
||
| `Ecs` | `Relay` (résistance à paliers) · `SgReady` (ballon thermodynamique) · `Variable` (triac) |
|
||
| `Hvac` | `SgReady` (PAC) · `ModbusSetpoint` (PAC, clim, VMC) |
|
||
| `Ev` | `Relay` (prise commandée : GreenUp, Witty) · `ModbusSetpoint` (borne) |
|
||
| `SmartHome` | `Relay` — voir §7, sémantique différente |
|
||
|
||
**LM-200 — Une case vide se lit de deux façons, et il faut les distinguer.** La
|
||
plupart sont *non prévues* : rien ne les interdit, elles n'ont simplement pas
|
||
d'usage identifié. D'autres sont **impossibles**, parce que le mécanisme ne sait
|
||
pas commander cet organe-là. Les confondre revient à traiter une contrainte
|
||
physique comme une lacune de feuille de route.
|
||
|
||
**Le cas dur, et le seul identifié à ce jour : la PAC.** Une pompe à chaleur se
|
||
pilote de **deux** façons, et de deux seulement :
|
||
|
||
| Voie | Mécanisme | Ce qui circule |
|
||
|---|---|---|
|
||
| **SG-Ready** | `SgReady` | deux contacts **secs** vers l'entrée de signalisation — 4 états normés |
|
||
| **Communicante** | mécanisme *communicant*, via un plugin nymea | mode, consignes, limite de puissance — le modèle **propre** de la machine |
|
||
|
||
**Ni `Relay`, ni `Variable`.** Ce ne sont pas des cases à remplir plus tard :
|
||
|
||
- **`Relay` couperait l'ALIMENTATION du compresseur.** Un `RelayRouter` étage de la
|
||
puissance réelle ; appliqué à une PAC, il lui retire son autorité sur son propre
|
||
cycle — dégivrage, retour d'huile, temporisation de redémarrage — et transforme
|
||
chaque décision d'arbitrage en court-cycling. Le verrou `minOn/minOff` limite la
|
||
casse, il ne rend pas le geste légitime.
|
||
- **`Variable` suppose un cadran en watts.** Un triac module une résistance ; un
|
||
compresseur n'est pas gradable. Une PAC communicante peut certes accepter une
|
||
*limite* de puissance, mais elle l'interprète selon son propre modèle et ne
|
||
s'engage sur aucune valeur instantanée — ce n'est pas la sémantique d'enveloppe
|
||
que `Variable` promet à l'optimiseur, et cette promesse alimente le budget.
|
||
|
||
**⚠️ Le piège de lecture : « ma PAC est pilotée par deux relais, donc `Relay` ».**
|
||
Non. Les contacts SG-Ready **sont** des relais, et au banc `.75` ce sont bien deux
|
||
things `power` (K1/K2). Mais ils **signalent**, ils ne **portent** pas la charge.
|
||
C'est exactement ce qui sépare les deux mécanismes : `Relay` commute de la
|
||
puissance, `SgReady` commute du sens. Voir des relais dans une configuration ne
|
||
dit donc rien du mécanisme qui convient.
|
||
|
||
**Ce que ça implique pour le mécanisme communicant, qui n'existe pas encore.** Il
|
||
est appelé `ModbusSetpoint` dans ce document : **le nom est trop étroit.** Ce qui
|
||
le définit n'est pas Modbus — c'est qu'un **thing nymea** porte le modèle de la
|
||
machine et que le moteur lui parle dans ce modèle-là. Le transport (Modbus
|
||
RTU/TCP, IP propriétaire, passerelle cloud) est une affaire de plugin
|
||
d'intégration, invisible d'ici. À renommer quand il sera implémenté ; à ne pas
|
||
confondre avec `Variable` d'ici là, malgré la ressemblance de surface d'une
|
||
« consigne » à écrire.
|
||
|
||
**Rien n'empêche aujourd'hui d'exprimer une case interdite, et c'est assumé.**
|
||
`domain` est une métadonnée d'intention, **optionnelle**, que l'arbitre ne lit
|
||
pas : une charge `domain: "hvac"` + `adapter: "relay-router"` est acceptée par
|
||
`isValid()`. **Décision prise (2026-08-25) : pas de refus.** Un refus ferait
|
||
cesser `o:domain` d'être une métadonnée — donc changerait le contrat annoncé à
|
||
l'app — et resterait **contournable par omission**, le domaine étant facultatif.
|
||
Un contrôle qu'on esquive en effaçant un champ protège mal. La sanction retenue
|
||
est un **avertissement au journal** à la construction de l'adaptateur.
|
||
|
||
**Cet avertissement n'a pas encore de prédicat, et ne peut donc pas être écrit
|
||
aujourd'hui.** LM-201 en dit la raison : le domaine décrit **ce qu'on arbitre**,
|
||
pas la machine — il ne peut pas révéler qu'il y a un compresseur derrière. Keyé
|
||
sur le domaine seul, l'avertissement crierait sur des installations correctes :
|
||
|
||
| Configuration | Verdict réel |
|
||
|---|---|
|
||
| `heating` + `relay-router` | **légitime** — chauffage électrique résistif sur relais |
|
||
| `hvac` + `relay-router` | **légitime** — VMC, ventilateur sur contact |
|
||
| `hvac` + `etmvariableload` | **légitime** — ventilation à vitesse variable |
|
||
| `heating`/`hvac` + relais, mais PAC derrière | ce qu'on veut signaler — **indistinguable des trois lignes ci-dessus** |
|
||
|
||
Le discriminant qui le porterait est **`typeAppareil`** (§3, LM-300), dont
|
||
l'énumération reste ouverte (§8-1) — c'est sa valeur *thermodynamique* qui dirait
|
||
« compresseur ». **L'avertissement est donc spécifié ici et implémenté avec
|
||
`typeAppareil`, pas avant.** Un avertissement qui crie au loup sur des
|
||
installations correctes abîme la seule surface de diagnostic dont dispose le banc.
|
||
|
||
---
|
||
|
||
**LM-201 — UN CANAL DE COMMANDE, UNE CHARGE.** Une charge existe quand il y a un
|
||
**canal de commande distinct pour un service**. C'est la règle qui fonde toutes
|
||
les autres, et elle repose la question du domaine : *« quel domaine pour une
|
||
PAC ? »* est **mal posée**. Une PAC air/eau fait chauffage, rafraîchissement et
|
||
ECS — le domaine ne décrit donc pas la **machine**, il décrit **ce qu'on
|
||
arbitre**.
|
||
|
||
Conséquences, toutes vérifiables sur une installation :
|
||
|
||
| Installation | Charges | Pourquoi |
|
||
|---|---|---|
|
||
| PAC chauffage + ECS, **sans ballon séparé** | **UNE** charge, domaine `heating` | un seul canal SG-Ready commande la machine entière |
|
||
| PAC + **ballon séparé** | **DEUX** charges (`heating`, `ecs`) | le ballon a son **propre** canal |
|
||
| PAC qui fait **aussi du froid** | **UNE** charge | même machine, même canal, même rang |
|
||
|
||
- **Pas de charge ECS sur une PAC sans ballon séparé.** Le SG-Ready commande la
|
||
machine entière ; la répartition entre chauffage et ECS appartient à la
|
||
**régulation de la PAC**. Il n'y a rien à arbitrer indépendamment — en déclarer
|
||
deux reviendrait à promettre un arbitrage qu'aucun canal ne peut exécuter.
|
||
- **Le froid ne crée pas de charge.** Ce qui change entre l'été et l'hiver est la
|
||
**saison**, pas la configuration. Une configuration qui devrait être réécrite
|
||
deux fois par an serait le signe qu'on a modélisé la machine et non le canal.
|
||
- **La contrainte d'exclusivité de `validateSet()` n'est pas une limite technique
|
||
subie — c'est le modèle.** Deux charges ne peuvent pas revendiquer les mêmes
|
||
contacts (LM-302-b, ECS-110-b) parce que **deux charges sur un canal unique se
|
||
contrediraient** : elles recevraient deux consignes pour un seul organe, et le
|
||
budget compterait la même puissance deux fois (règle absolue 1). Le refus
|
||
existait avant la règle ; la règle dit enfin pourquoi il est juste.
|
||
|
||
**LM-202 — Un thermostat de zone n'est PAS une charge.** Un thermostat ouvre une
|
||
**vanne** ; il ne porte pas la consommation, qui reste à la machine. Les déclarer
|
||
comme charges produirait des entrées à **0 W** dans le waterfall pendant que la
|
||
PAC est comptée ailleurs — des charges qui n'ont rien à recevoir, sur un canal
|
||
qui ne consomme rien.
|
||
|
||
Zones et thermostats sont de la **configuration de confort** : ils décrivent **où
|
||
va la chaleur**, pas **combien on en achète**. Ils vivent dans leur écran et leur
|
||
namespace — **pas dans `LoadConfig`**. C'est la même frontière que LM-102 trace
|
||
entre la couche domaine et l'allocation : décrire un besoin n'est pas négocier un
|
||
budget.
|
||
|
||
---
|
||
|
||
## §3 — Schéma de configuration (à figer)
|
||
|
||
**LM-300** — `LoadConfig` est une **union discriminée** par le mécanisme :
|
||
|
||
```
|
||
LoadConfig {
|
||
id, nom, enabled
|
||
domaine : Ecs | Hvac | Ev | SmartHome
|
||
typeAppareil : affine le domaine (résistif | thermodynamique | …)
|
||
groupe, prioritéGroupe, prioritéMembre
|
||
mécanisme : Relay | Variable | SgReady | ModbusSetpoint ← discriminant
|
||
chargeUtile : LoadConfigRelay | LoadConfigVariable | …
|
||
verrous : minOnS, minOffS
|
||
facettes[] : voir §4
|
||
}
|
||
```
|
||
|
||
**État d'implémentation (2026-08-09).** Trois charges utiles existent, discriminées
|
||
par le champ `adapter` du schéma persisté — `relay-router`, `etmvariableload`,
|
||
`sg-ready`. Le domaine, le groupe et les facettes ne sont pas encore portés : ce
|
||
qui est figé, c'est la **forme**, celle qui coûte cher à rattraper.
|
||
|
||
```
|
||
adapter = "sg-ready" → sgReady {
|
||
states[] { state : 1..4, relays[] : thingId, estimatedPowerW }
|
||
minStateHoldS
|
||
}
|
||
```
|
||
|
||
`estimatedPowerW` porte volontairement ce nom : c'est une **estimation**, jamais un
|
||
engagement — une PAC ne consomme pas la même chose à −5 °C et à +12 °C. Le champ
|
||
sert à ordonner et à budgéter, la mesure reste la source de vérité (ECS-500).
|
||
L'état 2 est **obligatoire** : c'est le plancher de repli du mode dégradé L2 et de
|
||
la désactivation (ECS-413/414). Une configuration qui ne peut pas l'exprimer est
|
||
refusée, pas construite.
|
||
|
||
**LM-301** — Ajouter un mécanisme DOIT se limiter à un type de charge utile et
|
||
une branche de fabrique. Rien d'autre ne bouge. **Vérifié à l'usage** : l'ajout de
|
||
`sg-ready` a touché exactement une charge utile, une branche de `isValid()`, une
|
||
branche de fabrique dans `rebuildLoadAdapters()`, et un champ optionnel au schéma
|
||
RPC. Aucun code d'arbitrage n'a bougé.
|
||
|
||
**LM-301-b — La limite de LM-301, trouvée au lot B-bis (2026-08-25).** « Une charge utile, une
|
||
branche de fabrique » vaut pour **construire** un mécanisme, pas pour l'**alimenter en actions**.
|
||
Quand `sg-ready` a rejoint la table commune des charges arbitrées, un code d'arbitrage a bel et
|
||
bien dû bouger : le repli du mode dégradé L2 fabriquait un `Setpoint(0)` pour **toute** la table,
|
||
en présumant du kind. Une PAC de configuration recevait donc une action que `SgReadyAdapter`
|
||
rejette d'entrée — et rien d'autre : elle restait en **état 4 (forcé)** sous compteur muet, quand
|
||
`SAFETY.md` §L2 exige l'état 2. Le défaut ne se voyait pas dans la télémétrie, où le motif
|
||
`DEGRADED_L2` s'affichait correctement ; seule l'**écriture** manquait.
|
||
|
||
**Règle qui en sort** : tout code qui **fabrique** une action pour une charge quelconque DOIT
|
||
lire `descriptor().supportedKinds` et se refuser explicitement s'il n'y trouve rien de connu —
|
||
jamais présumer du kind à partir de la table où la charge se trouve. C'est le pendant, côté
|
||
actions, de ce que LM-302-b dit côté validation : ce qui est vrai « pour toutes les charges »
|
||
n'appartient à aucune charge utile.
|
||
|
||
**LM-302-b — Ce qu'une charge utile ne peut pas valider.** L'exclusivité d'un Thing est
|
||
une propriété de l'**ensemble** des charges, pas d'une charge : aucune charge utile ne
|
||
peut la vérifier, quelle que soit sa rigueur. Elle vit donc au niveau du store
|
||
(`validateSet()`), et tout mécanisme futur DOIT déclarer les Things qu'il revendique via
|
||
`LoadConfig::claimedThingIds()` — c'est le seul point à étendre, et l'oublier rend le
|
||
mécanisme invisible à la vérification. Voir ECS-110-b.
|
||
|
||
**LM-302 — Charges utiles auto-validantes, et portée de l'« inexprimable ».**
|
||
Chaque charge utile valide la sienne. Les états invalides doivent être inexprimables
|
||
dans les **types C++** et le **schéma persisté** ; à la frontière **JSON-RPC**, la
|
||
validation est **à l'exécution**, par contrainte nymea.
|
||
|
||
**Corollaire vérifié à l'usage (2026-08-09).** `GetLoadConfig` sérialise via le
|
||
méta-objet, donc **toutes** les propriétés déclarées apparaissent dans la sortie — y
|
||
compris la charge utile **vide** d'un autre mécanisme. Un schéma qui exige une clé
|
||
interne dès que la charge utile est présente rend donc impossible l'aller-retour
|
||
lire-puis-réécrire, c'est-à-dire le geste ordinaire de l'application, et le rejet
|
||
survient **avant** le handler.
|
||
|
||
**Règle.** Toutes les clés internes d'une charge utile optionnelle DOIVENT être
|
||
déclarées optionnelles (`o:`) au schéma RPC. L'exigence réelle vit dans `isValid()`,
|
||
jamais dans le schéma. Les clés internes d'une charge utile optionnelle contraignent
|
||
les charges des **autres** mécanismes — c'est le côté par lequel le mur se touche.
|
||
|
||
**Obligation permanente.** Tout mécanisme ajouté DOIT être accompagné d'un test
|
||
d'aller-retour `GetLoadConfig` → `SetLoadConfig` **inchangé**, vérifié échouant sans
|
||
le correctif. Sans cette obligation, le prochain mécanisme rejouera la scène.
|
||
|
||
**LM-303 — Verrous.** `typeAppareil` fournit des **défauts** (résistif ≈ 60 s,
|
||
thermodynamique ≈ 300 s), l'installateur peut les modifier, et c'est la valeur
|
||
**stockée** qui fait foi. Jamais de temporisation codée en dur.
|
||
|
||
**Préséance.** Les verrous déclarés au niveau de la **charge** s'appliquent par
|
||
défaut. Une charge utile PEUT en porter de plus fins — par relais, par registre —
|
||
et ceux-là **priment** sur le défaut de charge, pour le seul actionneur qu'ils
|
||
désignent.
|
||
|
||
Cette règle laisse `spec_ecs.md` §13-3 (ECS-307, grain des temporisations par
|
||
relais) entièrement ouvert : ajouter plus tard un champ optionnel dans une charge
|
||
utile est rétro-compatible, quelle que soit la réponse. Le schéma persisté n'a
|
||
donc pas à être « rendu tolérant » — c'est la préséance qui devait être écrite,
|
||
et elle l'est ici.
|
||
|
||
---
|
||
|
||
## §4 — Facettes : une machine, plusieurs fonctions
|
||
|
||
**LM-400** — Une machine physique est **une seule charge** : un rang, un jeu de
|
||
verrous, une ligne de budget. Une PAC assurant chauffage et ECS n'est **jamais**
|
||
déclarée deux fois.
|
||
|
||
**Justification.** Deux déclarations produiraient une double allocation du
|
||
surplus pour une seule consommation, deux rangs pour un seul comportement, et
|
||
deux jeux de verrous sur un seul compresseur — la protection anti court-cycling
|
||
tomberait précisément là où elle coûte le plus cher.
|
||
|
||
**LM-401** — Une charge PEUT porter **plusieurs facettes** au-dessus d'un
|
||
actionnement unique. Une PAC mixte porte une facette ECS (consigne, température
|
||
ballon) et une facette chauffage, avec un seul mécanisme.
|
||
|
||
**LM-402** — Les facettes servent à la lecture, au suivi et au calcul
|
||
d'`available`. Elles **ne portent pas de rang**. Une charge à deux facettes ne
|
||
peut pas être classée « après les chauffe-eau mais avant la clim » : si cette
|
||
granularité est nécessaire, il faut deux machines, pas deux entrées.
|
||
|
||
**LM-403 — Le mode est une sortie, pas une entrée.** SG-Ready applique un niveau
|
||
d'encouragement à la machine entière ; le choix de la fonction appartient au
|
||
régulateur interne. Le HEMS **observe** le mode courant, il ne le sélectionne
|
||
pas. Aucune conception ne DOIT supposer un aiguillage.
|
||
|
||
Ce que le mode apporte : `available` devient honnête. Machine en mode chauffage
|
||
→ la facette ECS n'absorbera rien quoi qu'on encourage → le waterfall passe au
|
||
suivant au lieu de le deviner.
|
||
|
||
**LM-404 — Biais par consigne : option avancée, hors périmètre pour l'instant.**
|
||
Monter `hotWaterSetpointTemperature` place le ballon sous consigne et la machine
|
||
bascule d'elle-même. C'est indirect, lent, et surtout **persistant** : une
|
||
consigne écrite le reste après un plantage ou un redémarrage, et elle écrase un
|
||
réglage d'installateur. Toute implémentation future DOIT comporter des bornes
|
||
dures, une politique de restauration et un chien de garde. À ne rouvrir que si le
|
||
terrain le justifie.
|
||
|
||
**LM-405 — Décomposition côté ems, pas côté plugin.** Le découpage en facettes
|
||
DOIT se faire dans l'ems au-dessus des états plats du thing, et non en forkant
|
||
les plugins constructeurs. Chaque constructeur expose une structure différente ;
|
||
l'abstraction doit vivre là où elle est indépendante du constructeur, sous peine
|
||
de la refaire N fois et de gérer deux formes du même concept.
|
||
|
||
**LM-406** — Si l'installation n'a pas de chauffe-eau distinct, il n'y a pas de
|
||
charge ECS : la facette ECS s'accroche à la charge PAC.
|
||
|
||
---
|
||
|
||
## §5 — Groupes et double rang
|
||
|
||
**LM-500** — Un groupe est une **clé de tri**, jamais un détenteur de budget. Le
|
||
waterfall trie sur le couple `(prioritéGroupe, prioritéMembre)` et reste
|
||
inchangé. Un groupe qui reçoit une allocation puis la redistribue est un second
|
||
décideur (règle absolue 1).
|
||
|
||
**LM-501** — Classer un ECS, une PAC et une clim les uns par rapport aux autres
|
||
**fonctionne déjà** avec la liste plate triée par `priority`. Les groupes ne sont
|
||
nécessaires qu'à l'échelle : plusieurs chauffe-eau, plusieurs bornes, plusieurs
|
||
zones, quand on veut déplacer une famille entière sans renuméroter.
|
||
|
||
**LM-502 — Plafond de groupe : optionnel.** Justifié quand les membres partagent
|
||
une limite électrique réelle (bornes sur un même câble ou un même abonnement).
|
||
Sans objet pour un groupe thermique, dont les membres sont sur des circuits
|
||
distincts. C'est le seul point où la boucle du waterfall change vraiment : elle
|
||
doit suivre l'engagement cumulé du groupe.
|
||
|
||
**LM-503** — Deux notions à **ne pas** confondre avec un rang :
|
||
|
||
- **Exclusion mutuelle** (clim et PAC en opposition dans une même zone) : une
|
||
contrainte entre appareils.
|
||
- **Équité interne** (trois chauffe-eau à rang égal, dont le troisième resterait
|
||
froid tout l'été) : une politique de rotation dans le tri.
|
||
|
||
**LM-504** — « HVAC » couvre chauffage, ventilation et climatisation ; y ranger
|
||
l'eau chaude sanitaire est un abus. Libellé recommandé côté installateur :
|
||
« Thermique » ou « Chauffage & eau chaude ».
|
||
|
||
---
|
||
|
||
## §6 — Nommage
|
||
|
||
**LM-600** — Les classes d'actionnement portent le nom du **mécanisme**, jamais
|
||
du domaine.
|
||
|
||
**LM-601** — Un renommage de cohérence est souhaitable. Trois classes sont
|
||
concernées, avec des échéances distinctes :
|
||
|
||
| Classe | Défaut | Débloqué après |
|
||
|---|---|---|
|
||
| `RelayRouter` | Ne suit pas la convention `*Adapter` de ses voisins | étape 3 de `spec_ecs.md` |
|
||
| `EtmVariableLoadAdapter` | Préfixe `Etm` redondant dans un dépôt ETM | étape 3 de `spec_ecs.md` |
|
||
| `EvAdapter` | **Nommé d'après un domaine** (§2 range `Ev` en domaine) | **3g** |
|
||
|
||
`EvAdapter` n'est pas une classe de domaine égarée dans la couche mécanisme :
|
||
c'est un **adaptateur de mécanisme mal nommé**. Il parle à l'interface
|
||
`evcharger` de nymea — un mécanisme au même titre que `SgReady`. Le correctif est
|
||
un renom, pas un redécoupage.
|
||
|
||
Aucun de ces renommages ne se fait avant son échéance. Pour `RelayRouter`,
|
||
renommer juste avant la restructuration de `m_relayMapping` double le bruit dans
|
||
l'historique ; pour `EvAdapter`, le câblage lui-même change en 3g (l'adaptateur
|
||
n'est aujourd'hui pas dispatché), et renommer une classe dont le branchement va
|
||
bouger produit le même bruit pour rien.
|
||
|
||
`BatteryAdapter` n'entre pas dans ce tableau : LM-700 le traite hors moule.
|
||
|
||
---
|
||
|
||
## §7 — Cas hors moule
|
||
|
||
**LM-700 — Batterie.** Une batterie n'est pas une charge avec un rang : elle est
|
||
aussi une **source**, capable de financer les autres charges (grid-funding, 3f).
|
||
Elle intervient à un autre moment de la cascade et ne se modélise pas comme un
|
||
consommateur classé. Onduleur hybride et AC-coupling sont bien deux mécanismes.
|
||
|
||
**LM-701 — SmartHome.** Un cycle de lave-vaisselle ne se descend pas à 0 W en
|
||
cours de route : c'est un engagement pris au démarrage, pas un verrou `minOn`.
|
||
La sémantique d'action n'est pas une enveloppe en watts mais un **décalage
|
||
temporel** — démarrer maintenant ou plus tard. À traiter comme un type d'action
|
||
distinct, pas comme une variante.
|
||
|
||
---
|
||
|
||
## §8 — Points ouverts
|
||
|
||
1. `typeAppareil` — quelle énumération exacte, et jusqu'où elle affine le
|
||
domaine. **A désormais un consommateur nommé** : c'est lui, et non `domain`,
|
||
qui porterait l'avertissement de LM-200 (valeur *thermodynamique* = il y a un
|
||
compresseur). Tant qu'il n'existe pas, l'avertissement n'a pas de prédicat.
|
||
2. Représentation persistée des facettes (§4) : imbriquées dans la charge, ou
|
||
table séparée référençant la charge.
|
||
3. Sémantique d'action du SmartHome (LM-701) : à spécifier avant toute
|
||
implémentation, elle ne se déduit pas du reste.
|
||
4. Rotation d'équité (LM-503) : politique à définir si le besoin se confirme.
|
||
5. **Autorisation de soutirage (LM-1005).** Quel objet exactement — un booléen d'horloge, une
|
||
fenêtre tarifaire, un coût plafond ? Le rule-based et le MILP n'en attendent pas la même
|
||
forme, et la choisir trop tôt contraindrait le second.
|
||
6. **Seuil et fenêtre du diagnostic de mesure (LM-1102).** « Trois cycles » est un point de
|
||
départ. La valeur juste dépend de l'inertie de la charge et de la latence du compteur — donc
|
||
des données que LM-1103 doit d'abord constituer.
|
||
7. ~~**Cible de confort : en énergie ou en grandeur physique ?**~~ **TRANCHÉ** (2026-08-26) —
|
||
voir LM-1008.
|
||
8. **`heating` vs `hvac` — ce que ces deux codes désignent.** Question **ouverte, et
|
||
volontairement non tranchée**. Elle ne se saura vraiment qu'en dessinant la **PAC
|
||
communicante** (Alpha Innotec, Aquarea), où les consignes ECS et chauffage sont enfin
|
||
**séparées** — donc où le canal de commande cesse d'être unique, et où LM-201 rend un
|
||
verdict différent. D'ici là, `o:domain` **garde son rôle actuel** : grouper l'ordre de
|
||
service, **sans force normative**. **Ne construire aucune règle dessus** — l'avertissement
|
||
de LM-200 attend `typeAppareil` précisément pour cette raison.
|
||
*(La question « le domaine doit-il devenir un discriminant de validation ? » est, elle,
|
||
tranchée : non — avertissement, pas refus. Voir LM-200.)*
|
||
|
||
---
|
||
|
||
## §9 — Ce que ce document ne change pas
|
||
|
||
- L'ordre de `specs/spec_ecs.md` §2 et ses arbitrages bloquants.
|
||
- Les règles absolues 1 à 10 d'`AGENTS.md`.
|
||
- Le découpage en vigueur : le scheduler alloue, `RelayRouter` traduit.
|
||
- Rien dans le code aujourd'hui.
|
||
|
||
> Les §10 à §13 qui suivent sont **postérieurs** à ces deux sections de clôture : ils ont été
|
||
> ajoutés en 0.2.0 sans renuméroter le document, pour ne pas casser les renvois existants vers
|
||
> « §8-1 ». Les points ouverts qu'ils soulèvent ont été ajoutés au §8.
|
||
|
||
---
|
||
|
||
## §10 — Besoin à deux niveaux : éco et confort
|
||
|
||
> **Statut : intention de conception.** Rien de ceci n'est implémenté. Le modèle actuel n'a
|
||
> qu'un seul niveau de besoin, et il est implicite : *tout ce que le surplus permet*.
|
||
|
||
**LM-1000 — Une charge thermique n'a pas un besoin binaire.** Elle en a deux, de natures
|
||
différentes :
|
||
|
||
- **Éco** — un **plancher à atteindre quoi qu'il arrive**. Un ballon qui n'a pas vu 50 °C de
|
||
la journée doit les voir, au réseau s'il le faut. C'est une **obligation d'énergie sur un
|
||
horizon**, pas une préférence d'allocation.
|
||
- **Confort** — un **bonus**, servi seulement quand le surplus abonde. Monter le même ballon
|
||
à 60 °C parce qu'il y a du soleil. Ne rien servir n'est pas un échec.
|
||
|
||
Les deux ne se distinguent pas par leur quantité mais par ce qui arrive **quand on ne les
|
||
sert pas** : rien pour le confort, une obligation reportée pour l'éco.
|
||
|
||
**LM-1001 — L'éco n'est pas un champ nouveau : c'est `needs` promu.** `LoadConfig` porte déjà
|
||
`needs { dailyDeadline, minEnergyWhPerDay }`. C'est **exactement** la forme d'un plancher éco :
|
||
une énergie, une échéance. Ce qui manque n'est pas la déclaration du plancher — c'est la
|
||
**cible de confort** et la **place des deux dans le waterfall**.
|
||
|
||
> N'ajouter un `ecoNeeds` séparé serait dupliquer `needs` sous un autre nom, et il faudrait
|
||
> ensuite expliquer lequel des deux fait foi.
|
||
|
||
**LM-1002 — Deux passes sur la MÊME liste ordonnée. Pas de second rang.**
|
||
|
||
```
|
||
passe 1 — ÉCO : parcourir les charges par rang croissant, servir leur plancher éco
|
||
passe 2 — CONFORT : reparcourir la MÊME liste, dans le MÊME ordre, avec le reliquat
|
||
```
|
||
|
||
Le rang garde un sens unique et un seul — l'ordre de service que l'installateur a posé. La
|
||
hiérarchie éco/confort le **surplombe** : elle dit *quel besoin* est servi avant, jamais
|
||
*quelle charge* passe devant.
|
||
|
||
**Ce que cela vaut, formellement** : un ordre lexicographique sur `(niveau, rang)`, où
|
||
`niveau ∈ {éco, confort}`. C'est un ordre **total**, dérivé du rang existant — pas un second
|
||
système de priorité.
|
||
|
||
> **Rejeté explicitement** : une paire `prioritéEco` / `prioritéConfort` par charge. Elle
|
||
> permettrait de contredire le rang, elle divergerait de lui à la première modification, et
|
||
> elle obligerait l'installateur à tenir deux classements cohérents dans sa tête.
|
||
|
||
**LM-1003 — Deux passes, mais UNE SEULE commande par charge et par cycle.** Les passes
|
||
calculent des **cibles**, elles ne commandent pas. La consigne appliquée à une charge est la
|
||
**somme** de ce que les deux passes lui ont attribué, arrondie une seule fois au mécanisme
|
||
(palier de relais, état SG-Ready, consigne continue), puis émise une seule fois.
|
||
|
||
C'est la règle qui empêche le défaut le plus probable de cette conception : un relais commandé
|
||
en passe 1 puis recommandé en passe 2, donc deux commutations dans le même cycle — du
|
||
court-cycling fabriqué par l'arbitre lui-même.
|
||
|
||
**LM-1004 — Le recrédit anti-clignotement s'attribue UNE fois.** La correction B rend à chaque
|
||
charge sa consommation de début de cycle avant d'arrondir. Avec deux passes, ce crédit doit
|
||
être attribué à la **première** passe où la charge apparaît, jamais aux deux : compté deux
|
||
fois, il fabriquerait du budget qui n'existe pas.
|
||
|
||
**LM-1005 — La frontière que ce modèle franchit, et il faut la nommer.**
|
||
|
||
Jusqu'ici l'arbitre **distribue un surplus**. Un plancher éco non atteint au coucher du soleil
|
||
signifie **soutirer au réseau** — donc décider d'**acheter**. L'arbitre cesse d'être un
|
||
répartiteur pour devenir un acheteur, et cela ouvre trois questions qu'il n'avait jamais eu à
|
||
poser : à quel **prix**, à quelle **heure**, sous quel **signal tarifaire** (heures creuses,
|
||
Tempo).
|
||
|
||
- **Le rule-based franchira cette frontière grossièrement**, et c'est acceptable : « plancher
|
||
non atteint à 22 h → chauffer ». Une règle d'horloge, pas une optimisation.
|
||
- **Le MILP la franchira vraiment** : c'est précisément le problème qu'il sait résoudre —
|
||
placer une énergie obligatoire dans une fenêtre, au coût minimal, sous contraintes.
|
||
|
||
**Conséquence de conception** : la passe 1 n'est **pas** bornée par le surplus. Elle est bornée
|
||
par une **autorisation de soutirage** — un objet distinct, que le rule-based rendra binaire et
|
||
que le MILP rendra continu. La passe 2, elle, reste strictement bornée par le reliquat de
|
||
surplus. **Ne pas confondre les deux bornes** : c'est ce qui distingue un plancher d'un bonus.
|
||
|
||
**LM-1006 — Deux couplages à traiter en même temps, sinon la conception est fausse.**
|
||
|
||
1. **Le raccordement.** Si tous les planchers éco se servent au réseau le même matin sans
|
||
soleil, ils s'additionnent sur le disjoncteur de branchement. La passe 1 doit donc être
|
||
bornée par la **limite de raccordement** — aujourd'hui `phasePowerLimit`, dont la protection
|
||
de surcharge ne déleste **que les bornes de recharge**. Éco et délestage général sont le
|
||
même lot ; les séparer livrerait un plancher capable de faire disjoncter une installation.
|
||
2. **L'ordre de service apparent.** Un plancher éco de rang 4 passe devant un confort de
|
||
rang 1. C'est voulu, et c'est **contre-intuitif** pour l'installateur qui vient de poser son
|
||
ordre. L'app doit le montrer — deux passes visibles, pas un classement unique qui aurait
|
||
l'air désobéi. Sans cela, le premier retour terrain sera « l'ordre n'est pas respecté ».
|
||
|
||
**LM-1006-1 — Le délestage n'est pas « la protection de surcharge » : c'est l'application d'un
|
||
PLAFOND DE SOUTIRAGE, quelle qu'en soit la source.** *(Note de conception, Patrick, 2026-08-27.
|
||
Ne déclenche aucun travail — elle existe pour que la porte reste ouverte.)*
|
||
|
||
Le §14a EnWG (Allemagne) permettra au gestionnaire de réseau d'imposer un **plafond de puissance
|
||
au point de livraison**, que l'EMS répartit ensuite librement sur les appareils. C'est le même
|
||
mécanisme que le délestage exigé par LM-1006 (couplage 1), avec une **source de commande
|
||
différente**.
|
||
|
||
Il y a donc **trois sources pour un seul mécanisme** :
|
||
|
||
| Source | Nature du plafond | Durée |
|
||
|---|---|---|
|
||
| **Disjoncteur de branchement** | physique — `phasePowerLimit` | permanent |
|
||
| **§14a EnWG** | externe, **sur ordre** du gestionnaire de réseau | temporaire |
|
||
| **§10 éco** (LM-1005) | implicite — celui qu'on **s'impose** en décidant de soutirer | le temps de la passe 1 |
|
||
|
||
**Ce qui est demandé, et c'est tout** : quand le délestage général sera écrit, ne pas l'écrire
|
||
comme *« la protection de surcharge »* mais comme *« appliquer un plafond de soutirage »*. La
|
||
source est un paramètre, pas une hypothèse enfouie dans le nom de la fonction. **Cela tient dans
|
||
une signature aujourd'hui ; ce serait une refonte dans deux ans.**
|
||
|
||
Deux propriétés sont vraies des **trois** sources. Les tenir dès l'écriture ne coûte rien ; les
|
||
rattraper après coûte le mécanisme entier.
|
||
|
||
1. **Le plafond le plus contraignant l'emporte — et on doit pouvoir dire LEQUEL a mordu.**
|
||
C'est exactement l'exigence qui sépare `BELOW_MIN_POWER` de `SURPLUS_INSUFFICIENT` (§12), et
|
||
elle se transpose telle quelle : un client bridé doit lire **« gestionnaire de réseau »** et
|
||
non « protection de surcharge ». **Deux causes, deux gestes** — l'une se subit et se
|
||
renseigne auprès du fournisseur, l'autre s'attaque en délestant la maison ou en revoyant
|
||
l'abonnement. Un motif qui les confond envoie diagnostiquer le mauvais problème, avec
|
||
l'autorité d'une explication (règle 7-b d'`AGENTS.md`).
|
||
|
||
> Conséquence pratique : le plafond retenu et sa **source** appartiennent au motif publié,
|
||
> pas au seul journal. Le catalogue de motifs est fermé et testé sur le TEXTE — c'est le bon
|
||
> endroit pour que cette distinction ne puisse pas se perdre.
|
||
|
||
2. **Le plafond survit à la panne du HEMS** — mais **pas de la même façon selon la source**, et
|
||
les confondre ferait hériter au §10 d'une exigence qui ne le concerne pas encore.
|
||
*(Distinction posée par Patrick, 2026-08-27.)*
|
||
|
||
| Source | Ce que « survivre » veut dire | Qui doit le tenir |
|
||
|---|---|---|
|
||
| Disjoncteur | rien à faire — il coupe physiquement | **L0**, hors logiciel |
|
||
| **Plafond éco (§10)** | **rien à faire non plus** | le repli L2 **ne dépense pas** : un plafond qu'on s'impose à soi-même est respecté par construction dès lors qu'on cesse d'acheter |
|
||
| **§14a EnWG** | le plafond doit **continuer d'être appliqué** alors que le moteur ne décide plus, et la coupure totale est interdite | **hors du moteur** — le repli L2 n'en est pas capable, et n'a pas à l'être |
|
||
|
||
**Conséquence pour le §10 : aucune.** Son plafond est **auto-imposé**, donc un repli qui
|
||
n'initie rien (`docs/SAFETY.md` §L2) le respecte sans code supplémentaire. L'exigence dure —
|
||
tenir une borne imposée par un tiers, sans moteur — appartient au §14a, dont le transport
|
||
n'existe pas encore, et elle vivra **hors du moteur**. C'est ce que cette note réserve, et
|
||
c'est tout ce qu'elle réserve.
|
||
|
||
**Ce que cette note ne demande PAS** : aucun champ de configuration, aucune entrée au modèle de
|
||
charges, aucun code. Le §14a n'a pas de transport ici (il arrivera par un canal qui n'existe pas
|
||
encore), et le §10 n'est pas parti. C'est une **contrainte de forme sur du code non écrit**.
|
||
|
||
---
|
||
|
||
**LM-1008 — La cible de confort : les deux unités, mais pas au même endroit.** *(Décision
|
||
Patrick, 2026-08-26.)*
|
||
|
||
- **La frontière de l'arbitre reste en ÉNERGIE.** Rien ne change à ce que le moteur reçoit et
|
||
distribue : des watts et des Wh.
|
||
- **La cible se SAISIT en degrés**, parce que c'est ainsi que l'installateur et le client la
|
||
pensent. Un ballon se règle à 60 °C, pas à 2 400 Wh.
|
||
- **La conversion appartient à la couche domaine**, jamais à l'arbitre. C'est LM-100 appliqué
|
||
tel quel : le domaine détient le modèle de la machine, l'arbitrage ne connaît que des watts.
|
||
|
||
**Et aujourd'hui, aucune conversion n'est nécessaire.** Sans modèle thermique, il suffit de
|
||
savoir si le besoin de confort **existe encore** :
|
||
|
||
```
|
||
sonde < consigne de confort → la charge est demandeuse de son bonus
|
||
sonde ≥ consigne de confort → elle ne l'est plus, la passe 2 la saute
|
||
```
|
||
|
||
**C'est l'ARRÊT qui compte, pas le dosage.** Un bonus qu'on sert tant qu'il est utile et qu'on
|
||
cesse de servir quand il ne l'est plus se contente d'un comparateur — et un comparateur ne
|
||
demande aucun modèle.
|
||
|
||
Le **dosage en Wh** viendra avec le modèle thermique, que l'historique du §11 permettra
|
||
précisément d'apprendre. **Une raison de plus de faire le §11 en premier** : il ne prépare pas
|
||
seulement les seuils, il prépare la seule chose qui rendra le dosage possible.
|
||
|
||
**LM-1009 — Une échéance ne vaut que ce que son avancement se mesure.** *(Question remontée par
|
||
l'agent app, 2026-08-27. Note de conception : ne déclenche aucun travail, mais elle CONDITIONNE
|
||
la forme du §10.)*
|
||
|
||
**Le constat de l'app, et il est exact.** Sur `.75`, aucune classe `evcharger` ne déclare d'état
|
||
de charge et aucune ne porte d'interface véhicule — vérifié sur les cinq classes présentes
|
||
(`terraAcTcp`, `terraAcRtu`, `wallbox` ×2, `wallboxNoMeter`) : `batteryLevel` absent partout.
|
||
Les bornes publient `pluggedIn`, `charging`, `maxChargingCurrent`, `phaseCount` et, pour quatre
|
||
d'entre elles, `sessionEnergy`. Jamais un pourcentage.
|
||
|
||
**Ce que le code fait aujourd'hui, et c'est plus troublant que « il manque une mesure ».**
|
||
`targetPercentage` est comparé à `carBatteryLevel`, lu sur le Thing **voiture**
|
||
(`smartchargingmanager.cpp:628`). Et ce `batteryLevel`, quand la voiture est une voiture
|
||
*manuelle* — le seul cas où le mécanisme s'active, il exige un `batteryLevel` **inscriptible** —
|
||
est **écrit par le moteur lui-même** : `updateManualSoCsWithMeter()` intègre la puissance de la
|
||
BORNE, applique un facteur de pertes, divise par une `capacity` déclarée, et **réinjecte le
|
||
résultat dans l'état du Thing** par une Action.
|
||
|
||
Autrement dit : le moteur estime un pourcentage à partir de trois grandeurs dont **aucune n'est
|
||
mesurée** — le SoC de départ saisi par l'utilisateur, la capacité déclarée, un facteur de pertes
|
||
constant — puis compare la cible à sa propre estimation. **La boucle se referme sur elle-même**,
|
||
et rien, à aucun moment, ne lit l'état réel d'un véhicule.
|
||
|
||
> Le piège est là, et il touche l'app en premier : l'estimation est écrite dans `batteryLevel`,
|
||
> donc elle sort par la frontière RPC **indistinguable d'une mesure**. Un écran qui affiche
|
||
> « 62 % » affiche une intégration, pas une lecture. Si le §10 conserve ce chemin, ce champ doit
|
||
> être marqué comme ESTIMÉ là où il est publié — sans quoi l'app ment de bonne foi.
|
||
|
||
**Et pendant ce temps, la grandeur qui EST mesurée n'est lue par personne.** `sessionEnergy` —
|
||
l'énergie livrée depuis le branchement, comptée par la borne — est publiée par quatre des cinq
|
||
classes du banc. Le contrat porte déjà son emplacement : `LoadContextTelemetry::sessionWh`
|
||
(`surpluscontext.h:77`). **Il n'est rempli nulle part**, et `EvCharger` n'expose même pas
|
||
d'accesseur. Champ déclaré, jamais alimenté.
|
||
|
||
### Réponse à la première question : OUI, et le modèle a déjà la bonne forme
|
||
|
||
Un avancement utilisable se reconstruit sans communication véhicule, et les trois pièces
|
||
existent déjà :
|
||
|
||
| Ce qu'il faut | Ce qui existe | État |
|
||
|---|---|---|
|
||
| une grandeur mesurée | `sessionEnergy` de la borne | publiée, **jamais lue** |
|
||
| un emplacement au contrat | `LoadContextTelemetry::sessionWh` | déclaré, **jamais rempli** |
|
||
| une cible dans la même unité | `needs { minEnergyWhPerDay }` (LM-1001) | déclaré, **jamais lu par le scheduler** |
|
||
|
||
**C'est la forme que le §10 doit retenir** : une obligation exprimée en **énergie**, mesurée par
|
||
l'énergie livrée. Elle est vérifiable de bout en bout, et elle est déjà commune à toutes les
|
||
charges — un ballon et une borne portent le même `needs`, ce qui est exactement ce que LM-1001
|
||
annonçait.
|
||
|
||
**Quatre réserves, et chacune devient un défaut si on l'oublie :**
|
||
|
||
1. **Livré ≠ stocké.** `sessionEnergy` est comptée à la borne ; les pertes de charge font que le
|
||
véhicule en reçoit moins. Ce n'est pas un défaut de la mesure — c'est le bon côté du compteur
|
||
pour une **obligation d'énergie** : celui qu'on paie. Mais la promesse doit se formuler
|
||
« livrer X kWh », jamais « mettre X kWh dans la batterie ».
|
||
2. **`sessionEnergy` est une grandeur de SESSION** : elle se remet à zéro au branchement. Ce
|
||
n'est une réserve que si l'on vise une obligation *quotidienne* — et la décision D2 ci-dessous
|
||
ne la vise pas. Pour une obligation *par session*, la remise à zéro n'est pas un obstacle
|
||
contourné : c'est **exactement la bonne sémantique**, et le problème disparaît au lieu d'être
|
||
traité.
|
||
3. **Elle ne répondra JAMAIS à « la voiture sera-t-elle pleine ? »** Aucune énergie livrée ne dit
|
||
où en est une batterie dont on ne connaît ni l'état de départ ni la capacité réelle. Une cible
|
||
en énergie est vérifiable ; une cible en pourcentage ne l'est pas. Ce sont **deux objets
|
||
différents**, et le §10 ne doit pas les confondre sous le mot « échéance ».
|
||
|
||
### Réponse à la seconde question : OUI, et elle tient MÊME avec la première
|
||
|
||
Répondre « oui » à la première ne dispense pas d'écrire la seconde, parce que `targetPercentage`
|
||
**ne disparaît pas** : c'est un champ amont, c'est ce que l'app affiche, et c'est ce que
|
||
l'utilisateur saisit. Deux objets, donc deux régimes de vérité, et le motif publié doit les
|
||
distinguer :
|
||
|
||
| Objet | Vérifiable ? | Ce que le motif a le droit de dire |
|
||
|---|---|---|
|
||
| obligation en **énergie** (`needs`) | **oui**, par `sessionEnergy` | « X kWh livrés sur les Y demandés » — et l'atteinte peut être annoncée |
|
||
| cible en **pourcentage** (`targetPercentage`) | **non**, sans communication véhicule | l'intention déclarée, et **l'avancement comme inconnu ou estimé** — jamais comme tenu |
|
||
|
||
**Règle à tenir dès l'écriture** : *un motif n'affirme pas ce que le moteur ne mesure pas.* Une
|
||
échéance qu'on ne peut pas vérifier ne doit jamais se présenter comme tenue, ni comme en voie de
|
||
l'être. C'est la même exigence que celle qui sépare `BELOW_MIN_POWER` de `SURPLUS_INSUFFICIENT`
|
||
(§12) et que LM-1006-1 pose pour la source d'un plafond : **deux causes, deux phrases**, parce
|
||
qu'elles n'appellent pas le même geste. Ici le geste diffère radicalement — « attends, ça
|
||
charge » contre « je ne peux pas savoir où tu en es ».
|
||
|
||
### DÉCISIONS *(Patrick, 2026-08-27)*
|
||
|
||
**D1 — L'avancement s'affiche en ÉNERGIE LIVRÉE, jamais en pourcentage.** « 8,4 kWh livrés depuis
|
||
le branchement » est vérifiable ; « 62 % » suppose une capacité que personne ne connaît et **se
|
||
lira comme une lecture du véhicule**. Le pourcentage garde sa place dans la **saisie de
|
||
l'intention** — l'utilisateur pense sa voiture en pourcentage — mais **jamais dans l'affichage de
|
||
l'avancement**.
|
||
|
||
**D2 — Obligation par SESSION d'abord.** Le §10 lit `sessionEnergy` et s'arrête là. Trois raisons
|
||
qui vont dans le même sens : elle ne demande que de lire un champ **déjà publié** ; la remise à
|
||
zéro au branchement cesse d'être une réserve (voir ci-dessus) ; et c'est **l'usage réel** — on
|
||
branche le soir pour partir le matin. L'accumulation inter-sessions est un **lot à part**,
|
||
dépendant de l'historique du §11, et `minEnergyWhPerDay` ne sera honoré qu'avec lui.
|
||
|
||
**D3 — Tant que le chemin `carBatteryLevel` survit, il est ÉTIQUETÉ à la publication.** Une valeur
|
||
que le moteur a lui-même intégrée ne franchit pas la frontière RPC **sous le même nom qu'une
|
||
mesure**. Le précédent de nommage existe déjà dans ce document : `estimatedPowerW` porte
|
||
volontairement son préfixe (§3), parce qu'un nom doit porter le statut épistémique de ce qu'il
|
||
transporte. C'est aussi le refus opposé au `currentPowerStorage` à zéro du relevé 3g-1 : **mieux
|
||
vaut « estimé », ou rien, qu'un chiffre crédible et faux** — et c'est le corollaire de forme
|
||
d'`AGENTS.md` invariant 12, « un champ absent vaut mieux qu'un champ nul ou forgé ».
|
||
|
||
### CONDITIONS DE LIVRAISON — pas des recommandations
|
||
|
||
Elles s'écrivent avec le mécanisme, pas après. Ce sont les deux façons dont ce §10 peut mentir
|
||
sans erreur visible.
|
||
|
||
**C1 — Une borne qui ne publie pas `sessionEnergy` DÉGRADE VISIBLEMENT.** Mesuré sur le banc :
|
||
`wallboxNoMeter` et l'une des deux classes `wallbox` n'ont pas cet état. Sur ces bornes,
|
||
l'obligation n'est **pas mesurable** — et une échéance qui cesse silencieusement de l'être est
|
||
**pire qu'une échéance absente**, parce qu'elle continue de s'afficher comme si elle tenait. Le
|
||
moteur doit le dire, l'app doit le montrer, et la dégradation ne doit jamais se lire comme un
|
||
avancement nul. Même geste que `hasPowerMeter()`, déjà testé avant toute estimation de SoC.
|
||
|
||
**C2 — On doit pouvoir dire LEQUEL des deux régimes s'applique — dans le motif comme à l'écran.**
|
||
Un avancement mesuré et un avancement estimé ne se distinguent par aucune de leurs valeurs : les
|
||
deux sont un nombre qui monte. Si le régime ne se lit pas, le second se fait passer pour le
|
||
premier, et c'est précisément le défaut que cette note décrit. Le régime est donc une donnée
|
||
publiée, au même titre que la source d'un plafond en LM-1006-1 — **deux causes, deux phrases**.
|
||
|
||
---
|
||
|
||
**LM-1007 — Compatibilité descendante, et elle est gratuite.** Une charge qui ne déclare
|
||
**aucun** besoin éco ne participe qu'à la passe 2 — soit exactement le comportement
|
||
d'aujourd'hui. Le modèle à deux niveaux **dégrade proprement** vers le modèle actuel, ce qui
|
||
permet de l'implémenter sans migration de configuration.
|
||
|
||
---
|
||
|
||
## §11 — Compteur et sonde par charge
|
||
|
||
> **Statut : intention de conception.** Deux champs optionnels, de **même forme**, et dont
|
||
> l'arbitre ne se sert **pas pour décider** dans ce lot.
|
||
|
||
**LM-1100 — Deux champs, une même règle.**
|
||
|
||
```
|
||
LoadConfig {
|
||
…
|
||
o:meterThingId : ThingId — mesure de CETTE charge
|
||
o:sensorThingId : ThingId — grandeur physique du service rendu (température)
|
||
}
|
||
```
|
||
|
||
**Sélection par interface nymea, jamais par plugin.** Le compteur se choisit parmi les Things
|
||
portant `energymeter` / `smartmeter` ; la sonde parmi celles portant l'interface de température.
|
||
Nommer un plugin dans le modèle le rendrait faux au prochain plugin qui rend le même service —
|
||
et obligerait à maintenir une table à la main, ce que LM-600 refuse déjà pour les mécanismes.
|
||
|
||
> ⚠️ **`meterThingId` n'est plus la SEULE source de mesure — voir LM-1105.** Il l'a été, et cela
|
||
> obligeait l'installateur à désigner une borne comme son propre compteur pour qu'elle affiche
|
||
> ce qu'elle mesure déjà. Le champ garde tout son sens ; il cesse d'être une corvée de
|
||
> configuration là où le moteur peut lire l'appareil directement.
|
||
|
||
**LM-1101 — Le compteur sert à VÉRIFIER, jamais à décider.** Commandé 3 000 W, mesuré 0 W sur
|
||
trois cycles consécutifs : la charge ne fait pas ce qu'on lui demande, et c'est **la seule
|
||
chose** que la mesure autorise à conclure.
|
||
|
||
**Ne PAS injecter la mesure dans le budget.** Deux raisons, et chacune suffit :
|
||
|
||
1. **Elle est en retard sur la commande** — scrutation Modbus, moyennage interne du compteur.
|
||
L'injecter brute réintroduirait exactement l'oscillation que le recrédit de la correction B
|
||
a supprimée : commander, mesurer trop bas, recommander, dépasser, corriger.
|
||
2. **La charge est déjà comptée dans le compteur racine.** Le compteur de charge est une
|
||
**décomposition** de ce que le racine mesure, pas une source à additionner. L'ajouter
|
||
compterait la même énergie deux fois.
|
||
|
||
**LM-1102 — Un écart de mesure n'est pas un défaut d'écriture.** `WRITE_FAILED` dit « l'échelle
|
||
de commande est épuisée » ; un écart de mesure dit « la commande est partie, l'effet n'est pas
|
||
là ». Deux causes, deux gestes chez le client : l'une envoie au bus, l'autre au disjoncteur
|
||
divisionnaire ou à la résistance.
|
||
|
||
**L'écart NE DOIT PAS poser `available: false`.** Sortir une charge de l'arbitrage sur un doute
|
||
de **mesure** — alors que la commande, elle, est peut-être partie — coûterait le service sur un
|
||
soupçon. Il se publie comme **diagnostic**, à côté de `faultCode` et sans le remplacer.
|
||
|
||
> Le seuil et le nombre de cycles sont des **paramètres**, pas des constantes : un compteur
|
||
> lent et une charge à forte inertie produiraient sinon un diagnostic permanent. Trois cycles
|
||
> est un point de départ, pas une valeur figée.
|
||
|
||
> **Et l'écart ne CONCLUT PAS la même chose selon la source de la mesure** — LM-1106. Un écart
|
||
> calculé sur une valeur qui n'est pas une mesure ne conclut, lui, rien du tout.
|
||
|
||
**LM-1103 — La sonde est en lecture seule, et son vrai usage est différé.** Publiée en
|
||
télémétrie, **aucun effet** sur les décisions. Deux usages, dont le second est le plus
|
||
important :
|
||
|
||
1. **Diagnostic de mise en service** — l'installateur voit la température monter pendant qu'il
|
||
commande, ou ne pas monter.
|
||
2. **Constituer l'historique** — température, puissance, heure. **Sans lui, les seuils éco et
|
||
confort du §10 ne pourront jamais être réglés autrement qu'au doigt mouillé.** Un ballon ne
|
||
révèle sa constante de temps qu'observé, et une PAC ne révèle sa courbe qu'à travers une
|
||
saison. C'est la raison pour laquelle ce champ mérite d'exister **avant** d'avoir un
|
||
consommateur : le jour où le §10 s'implémente, il faut déjà des données.
|
||
|
||
**LM-1104 — Ni le compteur ni la sonde ne sont exclusifs, contrairement aux relais.** LM-201
|
||
(« un canal de commande, une charge ») ne s'applique pas ici : ces Things ne **commandent**
|
||
rien, ils mesurent. Deux charges peuvent légitimement partager une sonde de pièce, et un
|
||
compteur de tableau divisionnaire peut couvrir plusieurs charges.
|
||
|
||
**Conséquence directe** : `validateSet()` ne doit **pas** inclure `meterThingId` ni
|
||
`sensorThingId` dans son contrôle d'unicité des Things. Les y ajouter par symétrie avec
|
||
`relays[]` refuserait des configurations correctes.
|
||
|
||
**LM-1105 — Le moteur lit l'appareil qu'il pilote. `meterThingId` cesse d'être la seule
|
||
source.** *(Constat d'usage de l'agent app, 2026-08-28 ; proposition retenue.)*
|
||
|
||
Pour qu'une charge publie ce qu'elle consomme, il fallait la désigner **comme son propre
|
||
compteur** dans `meterThingId`. Ce n'est devinable par personne : un installateur voit un bloc
|
||
« compteur dédié » sur une borne qui publie déjà `currentPower`, et n'a aucune raison d'y
|
||
remettre la borne elle-même. **C'est de la configuration demandée pour compenser ce que le
|
||
moteur peut déduire** — et une configuration qu'on documente en note d'écran est le signe que le
|
||
défaut est dans le modèle, pas dans l'app.
|
||
|
||
**La règle** : quand le Thing piloté **porte lui-même l'état de puissance**, le moteur le lit,
|
||
sans configuration. `meterThingId` ne reste nécessaire que là où rien ne mesure.
|
||
|
||
> **La règle porte sur le THING, pas sur le mécanisme.** « Les bornes mesurent, les relais
|
||
> non » est vrai en pratique et faux en droit : certains `powerswitch` portent `currentPower`,
|
||
> et `RelayRouter` sait déjà les sommer. Écrire la règle par mécanisme y ajouterait la table à
|
||
> maintenir à la main que LM-600 refuse. Le test est une question posée à la **ThingClass**.
|
||
>
|
||
> **Et il se pose sur la CLASSE, jamais sur la valeur ni sur l'instant.** Un `relay-router` au
|
||
> palier 0 n'a aucun relais actif : interroger les relais *actifs* ferait osciller la réponse
|
||
> d'un cycle à l'autre, sur une installation dont rien n'a changé. La question est « ces Things
|
||
> portent-ils l'état ? », posée sur **tous** les Things déclarés de la charge.
|
||
>
|
||
> *Vérifié : jugée sur les relais actifs, la source publiée vaut `device` au palier 1 et `none`
|
||
> au palier 0 — un écran de mesure qui apparaît et disparaît au rythme du thermostat
|
||
> (`testMeasurementSourceIsPublished`, contre-épreuve).*
|
||
|
||
**LM-1105-a — Une mesure PARTIELLE n'est pas une mesure.** *(Affinement imposé par
|
||
l'implantation, 2026-08-28.)*
|
||
|
||
Là où plusieurs Things portent la charge — les relais d'un `relay-router` —, il faut qu'ils la
|
||
portent **tous**. « Au moins un » produirait une somme **sous-évaluée** présentée comme la mesure
|
||
de la charge : une topologie de trois relais dont un seul mesure annoncerait 500 W là où
|
||
1 500 W coulent, et l'écart commandé↔mesuré enverrait chercher une panne qui n'existe pas
|
||
(LM-1102). Une topologie mixte est donc `none` — et c'est un des cas où `meterThingId` garde
|
||
tout son sens.
|
||
|
||
> ⚠️ **À ne pas confondre avec `LoadTelemetry::currentPowerW`**, qui accepte « au moins un » et
|
||
> retombe sur le palier **nominal** sinon. Les deux ne répondent pas à la même question :
|
||
> celui-là est le **juge runtime du budget**, il doit rendre un nombre à chaque cycle parce que
|
||
> le recrédit anti-clignotement en dépend. Celui-ci ne vaut que ce qu'un appareil a réellement
|
||
> mesuré, ou rien. **Les fusionner rouvrirait la correction B.**
|
||
|
||
**LM-1105-b — La règle présuppose que le Thing piloté est TRAVERSÉ par la puissance.**
|
||
*(Trouvé en implantant, et c'est la limite exacte de LM-1105.)*
|
||
|
||
« Le moteur lit l'appareil qu'il pilote » n'est vrai que si l'appareil piloté est **dans le
|
||
chemin de puissance** de la charge. Un relais de `relay-router` commute la charge : sa puissance
|
||
**est** celle de la charge. Un contact **SG-Ready** est un contact de **SIGNAL** — la PAC est
|
||
alimentée ailleurs, son courant ne traverse jamais K1/K2, et un contact qui mesurerait quelque
|
||
chose mesurerait **sa propre bobine**.
|
||
|
||
**`sg-ready` est donc `none` par CONSTRUCTION**, et non par accident de matériel : même montée
|
||
sur des relais qui mesurent, la charge reste non mesurable par son organe de commande. C'est le
|
||
seul mécanisme pour lequel `meterThingId` demeure la **seule** voie vers une mesure — et cela
|
||
doit se lire à l'écran plutôt que se deviner.
|
||
|
||
**Le compteur explicite reste PRIORITAIRE, et ce n'est pas d'abord une question de précision.**
|
||
Un compteur divisionnaire posé exprès mesure mieux qu'un appareil qui se déclare — c'est vrai,
|
||
et ce n'est pas la raison principale. La raison principale est qu'**un réglage qui n'a pas
|
||
d'effet est pire que pas de réglage** : si le moteur préférait l'appareil au compteur que
|
||
l'installateur a pris la peine de désigner, le champ deviendrait décoratif, et le prochain à le
|
||
lire ne saurait pas s'il sert.
|
||
|
||
> **La priorité porte sur la SOURCE, pas sur la valeur.** `meterThingId` renseigné mais Thing
|
||
> absent, ou Thing sans l'état : on retombe sur l'appareil, et **ce qui est publié dit lequel a
|
||
> servi** (LM-1106). Ne rien publier au motif que le compteur déclaré est injoignable
|
||
> perdrait une mesure qu'on a — et le silence se lirait « charge non mesurable », ce qui serait
|
||
> faux.
|
||
|
||
**Ce que LM-1105 ne change PAS** : rien de ce que l'**arbitre** lit. LM-1101 tient mot pour
|
||
mot — la mesure vérifie, elle ne budgète pas — et le recrédit anti-clignotement continue de lire
|
||
`LoadContext::telemetry.currentPowerW`, qui vient de l'adaptateur et n'a jamais transité par
|
||
`meterThingId`. Ce lot déplace **ce qui est publié**, pas ce qui décide. En particulier, il ne
|
||
rouvre pas la correction B ni l'artefact du banc `.75` (`AGENTS.md`, PLAN 3C).
|
||
|
||
**LM-1106 — Ce qui est publié n'est pas un nombre : c'est un couple {valeur, source}.**
|
||
*(Second point soulevé par l'agent app, et c'est lui qui décide de la forme.)*
|
||
|
||
Le moteur porte **déjà** trois régimes sous un seul nom, et il jette la distinction :
|
||
|
||
| Adaptateur | Ce que vaut `telemetry().currentPowerW` | Régime |
|
||
|---|---|---|
|
||
| `evcharger` | `EvCharger::currentPower()` | **mesuré par l'appareil** |
|
||
| `etmvariableload` | l'état `currentPowerW` du Thing s'il existe ; sinon la consigne en vol ; sinon 0 | **mesuré**, ou **commandé** |
|
||
| `relay-router` | somme des relais qui portent `currentPower` ; **sinon le palier NOMINAL** | mesuré ou **nominal** — et le booléen `metered` est calculé, puis jeté |
|
||
| `sg-ready` | `estimatedPowerW[état]` | **déclaré**, jamais mesuré, délibérément (invariant 8) |
|
||
|
||
Publier ces quatre lignes sous le même `measuredW` serait faux pour deux d'entre elles. Et
|
||
publier un `measuredW` **automatiquement rempli** détruirait au passage la seule information que
|
||
le champ portait jusqu'ici : sa présence signifiait « un compteur a été posé exprès ».
|
||
|
||
**Donc le régime se PUBLIE, il ne se déduit pas d'une valeur.** C'est C2 de LM-1009, mot pour
|
||
mot, appliqué à la mesure — et pour la même raison qu'en LM-1009 : *un champ absent se voit ; un
|
||
champ qui s'efface se lit comme un zéro.*
|
||
|
||
```
|
||
o:measurement {
|
||
source : "meter" | "device" | "none",
|
||
o:powerW: double // absent si source == "none"
|
||
}
|
||
```
|
||
|
||
- **`meter`** — un Thing tiers déclaré par `meterThingId`.
|
||
- **`device`** — l'appareil piloté publie sa propre puissance.
|
||
- **`none`** — **rien ne mesure cette charge.** État à part entière, et positif : c'est C1 de
|
||
LM-1009 appliqué ici — une capacité manquante **se dit**. Sans lui, l'app ne peut pas
|
||
distinguer « pas mesurable » de « compteur en panne », et continue d'afficher un bloc de
|
||
mesure vide sur un `sg-ready` qui n'en aura jamais.
|
||
|
||
> **Et `none` ne se confond pas avec l'ABSENCE de la charge.** *(Distinction remontée par
|
||
> l'agent app, 2026-08-28.)* `none` est **publié**, donc il AFFIRME. L'absence n'affirme rien :
|
||
> elle dit que la charge n'a pas participé à l'arbitrage du cycle — mode manuel, aucun véhicule
|
||
> branché — ce qui est sans rapport avec sa mesurabilité. Une borne débranchée est absente de
|
||
> `loads[]` et publiera `device` dès qu'on y branchera une voiture ; lui annoncer « le compteur
|
||
> dédié est la seule voie » serait faux.
|
||
>
|
||
> C'est la même famille que LM-1009 D — *un champ absent se voit ; un champ qui s'efface se lit
|
||
> comme un zéro* — appliquée un cran plus haut : ici c'est l'ENTRÉE entière qui s'efface, et la
|
||
> lire comme un `none` transformerait un silence en affirmation. La garantie qui rend la règle
|
||
> utilisable : **`measurement` est présent pour toute charge de `loads[]`, et pour elle seule.**
|
||
|
||
> **Sous `none`, aucune valeur n'est publiée sous ce nom.** La puissance *nominale* existe et
|
||
> reste lisible — `mechanism.stageW`, `mechanism.state`, `mechanism.estimatedPowerW` la portent
|
||
> déjà, et disent d'où elle vient. La faire passer pour une mesure produirait un écart
|
||
> commande↔mesure **identiquement nul, à tous les cycles, pour toujours** : un « tout
|
||
> concorde » permanent, c'est-à-dire le pire des diagnostics. C'est le corollaire LM-104 —
|
||
> l'hypothèse de prudence devient un masquage de panne.
|
||
|
||
**Ce que la source change au DIAGNOSTIC** (LM-1102), et c'est ce qui la rend utile plutôt que
|
||
décorative :
|
||
|
||
| Source | Ce qu'un écart commandé↔mesuré autorise à conclure |
|
||
|---|---|
|
||
| `device` | **La charge n'obéit pas.** Commande et mesure sortent de la même boîte : l'écart parle de la charge et d'elle seule — véhicule qui refuse le courant, thermostat ouvert, plafond interne. |
|
||
| `meter` | **La charge, le compteur, ou le câblage.** Trois hypothèses, l'écart n'en tranche aucune seul. C'est le seul cas où « le compteur ne voit pas cette charge » est sur la table — le défaut ouvert du banc, compteur racine aveugle aux résistances ECS. |
|
||
| `none` | **Rien.** Aucun écart n'est calculable, et il ne faut surtout pas en fabriquer un. |
|
||
|
||
> **L'asymétrie va plus loin que « le compteur mesure mieux », et elle donne à LM-1104 sa
|
||
> raison d'être.** Sous `device`, une **concordance** ne prouve pas grand-chose : plusieurs
|
||
> wallbox renvoient leur consigne en guise de puissance, et commandé == mesuré n'y est qu'une
|
||
> tautologie. Sous `meter`, une concordance est une **preuve**. Autrement dit : `device`
|
||
> détecte la **désobéissance**, `meter` est le seul à pouvoir détecter un **mensonge**. C'est
|
||
> pourquoi `meterThingId` doit rester **autorisé** sur une charge qui publie déjà sa puissance
|
||
> — LM-1104 ne devient pas caduc, il gagne un usage.
|
||
|
||
**Conséquence pour l'app, et elle est de présentation, pas de modèle** : le champ « compteur
|
||
dédié » se **masque par défaut** là où `source` vaut `device`, et se propose en réglage avancé.
|
||
Il ne doit pas être *interdit* : ce serait fermer le cas ci-dessus, et casser le compteur de
|
||
tableau divisionnaire couvrant plusieurs charges (LM-1104).
|
||
|
||
> **La règle de présentation se lit sur `source`, JAMAIS sur `adapter`.** La tentation est
|
||
> d'écrire « masqué sauf pour `sg-ready` », puisque LM-1105-b fait de ce mécanisme une
|
||
> exception. Mais l'exception est déjà **dans la donnée** : un `sg-ready` publie `none`, donc la
|
||
> règle « masquer quand `source == "device"` » ne le masque pas — sans qu'aucun client ait à
|
||
> connaître le mécanisme.
|
||
>
|
||
> Et brancher sur `adapter` se **trompe** sur un cas qui n'a rien de théorique : un
|
||
> `relay-router` à topologie **mixte** publie lui aussi `none` (LM-1105-a). Un client qui ne
|
||
> déciderait qu'au mécanisme masquerait le champ sur un ECS qui, précisément, ne peut être
|
||
> mesuré que par un compteur dédié. Un mécanisme est un mauvais indice de mesurabilité ; la
|
||
> source EST la mesurabilité.
|
||
>
|
||
> C'est LM-1106 pris par l'autre bout — le régime est publié **pour** qu'il ne soit pas déduit,
|
||
> et le déduire du mécanisme côté client remettrait dans l'app la déduction qu'on vient de
|
||
> retirer du moteur.
|
||
|
||
---
|
||
|
||
## §12 — Le plancher de modulation
|
||
|
||
> **Statut : intention de conception.** C'est le **préalable à l'étape 3g** — la
|
||
> transplantation du VE dans le waterfall.
|
||
|
||
**LM-1200 — Le modèle n'exprime aujourd'hui qu'une enveloppe proportionnelle.** Une charge
|
||
modulable est décrite par `maxPowerW`, et l'arbitre suppose qu'elle accepte n'importe quoi
|
||
entre 0 et ce plafond. **C'est faux pour la plupart du matériel réel** :
|
||
|
||
- une borne de recharge **ne charge pas sous 6 A** — sous ce seuil elle ne démarre pas, ou
|
||
démarre et s'arrête ;
|
||
- les **paliers SG-Ready sont un plancher déguisé** : l'état 3 consomme ce qu'il consomme, il
|
||
n'existe pas d'état « 3 à moitié » ;
|
||
- un routeur de relais a le même plancher implicite : son plus petit palier non nul.
|
||
|
||
**LM-1201 — Spécifier : rien, ou au moins X.** Une charge modulable porte un minimum non nul :
|
||
|
||
```
|
||
adapter = "etmvariableload" → { mode: "dynamic", maxPowerW, o:minPowerW }
|
||
```
|
||
|
||
Le domaine de la consigne cesse d'être `[0, maxPowerW]` pour devenir
|
||
**`{0} ∪ [minPowerW, maxPowerW]`** — un ensemble discontinu. C'est la seule façon de dire ce
|
||
que le matériel fait vraiment.
|
||
|
||
**LM-1202 — Le plancher existe pour les trois mécanismes ; il n'est déclaré que pour un.**
|
||
|
||
| Mécanisme | Plancher | Origine |
|
||
|---|---|---|
|
||
| `etmvariableload` | `minPowerW` | **déclaré** — rien ne permet de le deviner |
|
||
| `relay-router` | plus petit palier non nul | **dérivé** de `relays[]` |
|
||
| `sg-ready` | `estimatedPowerW` de l'état 3 | **dérivé** de la table d'états |
|
||
|
||
Ne pas déclarer ce qui se dérive : c'est la règle que le lot D vient d'appliquer aux paliers
|
||
(`stagesW`), et pour la même raison — deux sources d'une même vérité divergent.
|
||
|
||
**LM-1203 — Un budget inférieur au plancher ne doit pas être retenu en otage.** Règle
|
||
d'allocation :
|
||
|
||
```
|
||
si budget_disponible < plancher(charge) → allouer 0, et TRANSMETTRE le budget à la suivante
|
||
```
|
||
|
||
Sans cette règle, une charge de rang 1 au plancher élevé bloquerait tout le waterfall en aval
|
||
en gardant un budget qu'elle ne peut pas utiliser.
|
||
|
||
**LM-1204 — Pourquoi c'est le préalable à 3g, et pas un raffinement.**
|
||
|
||
Deux bornes, 2 kW de surplus, plancher de 1,38 kW chacune (6 A monophasé) :
|
||
|
||
- **répartition égale** : 1 kW à chacune → **les deux sont sous leur plancher, rien ne charge,
|
||
et les 2 kW partent au réseau** ;
|
||
- **waterfall** : 1,38 kW à la première, le reliquat de 0,62 kW à la seconde qui n'atteint pas
|
||
son plancher et reçoit donc 0 → **une voiture charge**.
|
||
|
||
Le proxy actuel, qui décide du VE **en amont** du waterfall, ne peut pas produire le second
|
||
comportement : il n'a pas de notion de reliquat à passer. **C'est le plancher qui rend la
|
||
transplantation nécessaire, pas l'inverse.**
|
||
|
||
**LM-1203-b — Sous la réserve de stockage, le budget est ANNULÉ, pas réduit.** *(Décision
|
||
Patrick, 2026-08-26 ; ne se déduit pas du code.)*
|
||
|
||
Quand le niveau de la batterie passe sous sa réserve, le waterfall ne rabote pas le budget :
|
||
il le met à **zéro**. Servir « un peu » cumulerait deux inconvénients sans produire le
|
||
moindre avantage — cela retarderait la recharge du stockage **et** ne démarrerait
|
||
probablement personne, puisque les planchers du §12 mordent d'abord. Un budget résiduel de
|
||
300 W devant un premier palier de 500 W ne sert personne ; il ne fait que priver la batterie.
|
||
|
||
> **La réserve n'annule QUE le surplus — pas une autorisation de SOUTIRAGE.** *(Décision
|
||
> Patrick, 2026-08-27, à l'ouverture du §10.)* La passe éco du §10 ne dépense pas de surplus :
|
||
> elle dépense une autorisation d'acheter au réseau. La réserve ne la touche donc pas. Deux
|
||
> raisons, et chacune suffit : **une obligation qui cède à la réserve cesse d'être une
|
||
> obligation** — un ballon qui n'a pas vu 50 °C ne les verrait jamais parce que la batterie de
|
||
> la maison est basse ; et la réserve garde tout son sens là où elle en a un, **empêcher de
|
||
> vider la batterie dans un chauffe-eau**, ce qu'un soutirage réseau ne fait pas.
|
||
|
||
**Corollaire, et c'est le piège** : une installation dont la réserve est réglée haut ne pilote
|
||
plus RIEN sous ce niveau — ni ECS, ni PAC, ni borne. Cela ne produit ni erreur ni panne, donc
|
||
rien ne le signale. D'où deux exigences qui accompagnent la règle :
|
||
|
||
1. le motif **`BATTERY_RESERVE`** publie le surplus **retenu**, le SOC et le seuil — la cause
|
||
se lit en une ligne au lieu de se chercher ;
|
||
2. le **défaut d'usine est une réserve, pas une cible**. `0,9` confondait les deux : une cible
|
||
est ce qu'on aimerait atteindre, une réserve ce sous quoi on ne descend pas. Abaissé à
|
||
`0,2` en 3g-1.
|
||
|
||
**LM-1204-b — Le scénario d'acceptation de 3g, à monter au banc.** *(Défini avec Patrick,
|
||
2026-08-26, quand le banc a reçu une seconde borne.)*
|
||
|
||
> **MOITIÉ ARBITRAGE FRANCHIE (3g-2)** — `testEvChargerRankDecidesWhoCharges` : deux bornes, un
|
||
> budget qui ne paie qu'un plancher, la seconde refusée avec `BELOW_MIN_POWER {budgetW: 620,
|
||
> minPowerW: 1380}`, et l'inversion du rang change la borne servie. **La moitié MATÉRIELLE reste
|
||
> ouverte** : elle exige deux bornes RÉELLES, et la Terra AC ne répond toujours pas
|
||
> (`docs/RECAP_3g2.md` §1). Ce qu'une wallbox simulée ne prouvera pas est écrit plus bas — et
|
||
> c'est précisément ce qui manque encore.
|
||
>
|
||
> **Il n'était PAS exerçable avant 3g**, et c'est le point : aujourd'hui `nonEvLoads` filtre
|
||
> `etmvariableload | relay-router | sg-ready`, l'EV est décidé **en proxy avant** le waterfall,
|
||
> les deux bornes ne partagent donc aucun budget avec le reste et **leur rang n'a aucun sens**.
|
||
> Ce scénario est le critère qui dira que 3g est fait.
|
||
|
||
1. **Budget insuffisant pour deux planchers.** Surplus ≈ 4 kW, deux bornes à ≈ 1,4 kW de
|
||
plancher (6 A monophasé). **Attendu** : la borne de rang supérieur servie à fond, la seconde
|
||
au reliquat — et **0 si le reliquat est sous son plancher**, jamais une part symbolique.
|
||
**Contre-exemple à ne surtout pas produire** : 2 kW à chacune, les deux sous leur plancher,
|
||
rien ne charge et le surplus part au réseau.
|
||
2. **Inversion de rang entre les deux bornes**, et la charge servie doit changer. C'est le
|
||
pendant EV de l'inversion ECS/SG-Ready déjà éprouvée.
|
||
|
||
**Le piège de simulateur, et comment le déjouer.** Une wallbox simulée acceptera peut-être une
|
||
consigne sous son plancher là où du matériel réel refuserait de démarrer. **Le plancher doit
|
||
être appliqué par l'ARBITRE, jamais constaté par le matériel** — sinon le test ne prouve que
|
||
l'obéissance du simulateur.
|
||
|
||
Le discriminant est le **motif publié**, et il est décisif parce que seul le scheduler peut
|
||
l'émettre :
|
||
|
||
| ce qu'on lit | ce que ça prouve |
|
||
|---|---|
|
||
| `BELOW_MIN_POWER {budgetW, minPowerW}` | **l'arbitre a refusé** — aucune commande n'est partie |
|
||
| `SURPLUS_SETPOINT` avec `setpointW < minPowerW` | l'arbitre a laissé passer : le plancher n'est **pas** appliqué |
|
||
| borne à 0 W sans motif de plancher | c'est le **matériel** qui a refusé — le test ne prouve rien |
|
||
|
||
**Ce qu'une wallbox simulée ne prouvera pas**, et qu'il ne faut pas déclarer vérifié : la
|
||
négociation de courant réelle, le délai d'établissement de la charge, et le comportement d'un
|
||
véhicule qui refuse.
|
||
|
||
> ⚠️ **À lire avec LM-1009 (§10)** : la machinerie existe, mais son AVANCEMENT ne se mesure pas.
|
||
> `targetPercentage` est comparé à un pourcentage que le moteur a lui-même estimé, et la seule
|
||
> grandeur réellement mesurée — `sessionEnergy` — n'est lue nulle part. Une échéance en énergie
|
||
> est vérifiable ; une échéance en pourcentage ne l'est pas.
|
||
|
||
**LM-1205 — La machinerie d'échéance existe déjà.** `needs { dailyDeadline, minEnergyWhPerDay }`
|
||
est ce qui avait motivé le proxy — la crainte que le waterfall ne sache pas exprimer « cette
|
||
voiture doit être chargée à 7 h ». Il sait : c'est le même champ que le plancher éco du §10
|
||
(LM-1001). Une fois le VE dans le waterfall, il hérite du modèle de besoin commun.
|
||
|
||
> **LM-1206 et LM-1207 sont IMPLÉMENTÉS (3g-2).** Le rang d'une borne est un champ de
|
||
> `LoadConfig`, créé d'office à la détection, modifiable à chaud par `SetLoadConfig`. Aucun champ
|
||
> de rang n'a été ajouté à `ChargingInfo`, et il n'y en aura pas. Forme exacte de l'entrée :
|
||
> `INTERFACE.md`, « le mécanisme `evcharger` ».
|
||
|
||
**LM-1206 — La priorité porte sur la BORNE, pas sur la voiture. Décision arrêtée.** Il n'existe
|
||
pas de communication véhicule aujourd'hui : rien ne permet de savoir *quelle* voiture est
|
||
branchée, ni de lui attribuer un rang. Le rang appartient donc au **point de charge**, comme il
|
||
appartient à un chauffe-eau.
|
||
|
||
`assignedCarId` existe déjà dans `ChargingInfo` : le jour où la communication véhicule arrive,
|
||
la migration du rang de la borne vers le véhicule reste possible **sans casser l'acquis**.
|
||
|
||
**LM-1208 — Une charge sortie de la DÉCISION n'est jamais sortie de la COMPTABILITÉ.**
|
||
*(Règle posée par l'agent app, 2026-08-28, sur un constat de banc. La règle est retenue ; le
|
||
mécanisme qui l'accompagnait était inexact — voir l'encadré.)*
|
||
|
||
Quel que soit le motif d'exclusion — mode manuel, véhicule inconnu, borne débranchée — **la
|
||
consommation réelle de la charge reste dans le bilan du budget**. Une charge qu'on cesse de
|
||
piloter ne cesse pas de consommer.
|
||
|
||
> **Cette règle est DÉJÀ tenue, et par construction** : le budget part de la mesure **brute** du
|
||
> compteur (invariant 8 d'`AGENTS.md`), donc la consommation d'une charge exclue y est déjà
|
||
> déduite — sans que personne ait à la réserver. Vérifié au banc le 2026-08-28 : la V2C tirant
|
||
> 3,9 kW hors arbitrage, le compteur est passé de −2 915 W (export) à +3 531 W (import) et
|
||
> l'arbitre a publié « Surplus insuffisant (−3 531 W) — chauffe-eau à 0 W ». **L'ECS n'a pas été
|
||
> servi par-dessus.** L'exclusion est conservatrice : la charge exclue retire du budget aux
|
||
> autres, et personne ne le lui rend.
|
||
>
|
||
> **La règle garde pourtant un porteur, étroit et réel** : une charge exclue du contexte mais
|
||
> commandée par le **proxy** ne reçoit aucun `evReservedW` — sa puissance commandée et pas
|
||
> encore mesurée n'est réservée par personne (correction A ne couvre que les actions du
|
||
> scheduler). Le trou est transitoire — un cycle, jusqu'à ce que le compteur rattrape — et il ne
|
||
> mord pas aujourd'hui, le mode manuel faisant sauter la borne aussi côté proxy. **Il mordra au
|
||
> §10**, où la passe éco décide d'acheter : acheter en croyant disposer d'une puissance déjà
|
||
> engagée est exactement le défaut que la règle nomme.
|
||
|
||
**LM-1208-b — « Pas de voiture assignée » cesse d'être un critère d'exclusion.** *(Décision
|
||
demandée, argument de l'agent app retenu.)*
|
||
|
||
Trois raisons, et la troisième est décisive :
|
||
|
||
1. **Le cas de l'invité le rend intenable.** Une voiture qui n'est pas celle du propriétaire ne
|
||
sera jamais déclarée, et la borne cesserait d'être arbitrée **précisément quand elle
|
||
consomme**.
|
||
2. **Le critère n'est pas vérifiable.** L'assignation est déclarée à la main ; rien ne dit ce qui
|
||
est réellement branché. Un critère d'arbitrage qui repose sur une déclaration invérifiable
|
||
n'arbitre pas, il devine.
|
||
3. **Il CONTREDIT LM-1206.** Le rang et le plancher appartiennent à la **borne**, pas au
|
||
véhicule. Exiger un véhicule connu pour arbitrer la borne réintroduit exactement la
|
||
dépendance que LM-1206 a écartée.
|
||
|
||
**Ce qu'on perd sans voiture assignée est une FONCTION, pas l'arbitrage** : l'échéance et la
|
||
cible d'énergie deviennent indisponibles — dégradation, pas exclusion. C'est la même distinction
|
||
que C1 de LM-1009 : une capacité manquante se dit, elle ne fait pas disparaître la charge.
|
||
|
||
> **Ce qui RESTE un critère d'exclusion, et pourquoi** :
|
||
> — **mode manuel** (`ChargingModeNormal`) : c'est une intention explicite de l'utilisateur —
|
||
> il pilote, l'arbitre s'abstient. Vérifiable, et voulu.
|
||
> — **aucun véhicule branché** (`pluggedIn`) : la borne ne peut rien consommer de ce qu'on lui
|
||
> allouerait (mesuré, `docs/RELEVE_3g2.md` §3.1).
|
||
>
|
||
> ⚠️ **Conséquence d'exploitation à connaître** : le mode par défaut d'une borne fraîchement
|
||
> détectée est `ChargingModeNormal`. **Une borne neuve n'est donc jamais arbitrée tant que
|
||
> personne n'a choisi un mode éco** — ce qui est correct par construction, mais doit être visible
|
||
> à la mise en service, sans quoi « la borne ne charge pas toute seule » se lira comme un défaut.
|
||
|
||
**LM-1208-c — Sans voiture, le nombre de phases vient de la BORNE.** *(Conséquence de
|
||
LM-1208-b trouvée à la mise en œuvre, 2026-08-28. Elle n'est pas un détail d'implantation : elle
|
||
décide du budget.)*
|
||
|
||
Retirer le critère ne suffit pas, et ce qui manque ne se voit pas. Le proxy amont **ne prépare
|
||
pas** une borne sans voiture — `prepareInformation()` la filtre en entrée, et c'est correct :
|
||
sans véhicule il n'a ni SoC, ni capacité, ni échéance à planifier. La borne entre donc dans le
|
||
waterfall **sans `ChargingProcessInfo`**, c'est-à-dire sans la règle de bascule 1↔3 phases que
|
||
LM-1206 avait pris soin de ne pas redériver.
|
||
|
||
Or l'accesseur rend un objet **par défaut**, pas une absence — et ses valeurs par défaut sont
|
||
*plausibles* : une phase, pas de bascule. Une borne triphasée serait donc annoncée monophasée,
|
||
et **le budget divisé par trois** : plafond à 3 680 W au lieu de 11 040, courant commandé à 16 A
|
||
au lieu de 9, et 11 kW tirés d'une enveloppe qui en comptait 3,7. C'est le défaut de 3g-1
|
||
(« viser 3 phases sur une combinaison qui n'en permet qu'une ») pris exactement à l'envers.
|
||
|
||
**La règle** : sans `ChargingProcessInfo`, on **ne bascule pas** — basculer suppose de savoir ce
|
||
que la voiture accepte — et on retient `phaseCount` de la **borne**, qui est ce qu'elle utilisera
|
||
réellement. C'est l'hypothèse conservatrice au sens de LM-104 : elle **surestime** la
|
||
consommation de la borne plutôt que de la sous-estimer.
|
||
|
||
> **Ce que ce cas apprend au-delà des bornes** : un accesseur qui rend un défaut là où il
|
||
> faudrait dire « absent » est un masquage de panne (corollaire LM-104), et il est d'autant plus
|
||
> dangereux que le défaut est crédible. La question « cette borne a-t-elle été préparée ? » doit
|
||
> être POSABLE, pas déduite d'une valeur.
|
||
|
||
**LM-1209 — L'auto-provisionnement REMPLIT UN TROU, il ne décide pas.** *(Décision Patrick,
|
||
2026-08-28. La règle est générale : elle porte sur toute création d'entrée par le moteur, pas
|
||
sur les seules bornes.)*
|
||
|
||
Une charge détectée par le moteur reçoit une entrée de configuration parce que **sans entrée
|
||
elle serait arbitrée sans rang réglable** — c'est le trou que l'auto-provisionnement referme. Il
|
||
ne s'ensuit rien sur sa PLACE : **le rang est un choix de l'installateur**, et une détection ne
|
||
réordonne pas ce qu'un humain a décidé.
|
||
|
||
**Donc : insertion en QUEUE, jamais en tête.** Une borne détectée un mois après la mise en
|
||
service passerait sinon devant l'ECS que quelqu'un a réglé exprès — l'ancienneté d'un réglage
|
||
n'est pas une raison de le défaire, et la nouveauté d'une détection n'est pas une raison de la
|
||
servir en premier.
|
||
|
||
> **Cela ne contredit PAS l'ordre par défaut « le VE d'abord »**, et c'est la distinction qui
|
||
> manquait à la question : ce défaut-là décrit une **mise en service initiale**, où personne n'a
|
||
> encore rien classé. Un **ajout ultérieur** est une situation différente — il existe un
|
||
> classement, et il fait autorité. Deux moments, deux règles, et les confondre revient à laisser
|
||
> le matériel réordonner l'installation.
|
||
|
||
**Ce que « en queue » veut dire exactement : `1 + max(rangs existants)`.** Formulé d'abord comme
|
||
« en queue de son DOMAINE », il faut le préciser, parce que les domaines ne sont pas des blocs
|
||
contigus : rien n'empêche `ev` d'occuper les rangs 2 et 3 pendant qu'`ecs` tient 1 et 4. Insérer
|
||
« juste après le dernier `ev` » exigerait alors de pousser l'ECS du rang 4 — ce que LM-1209-b
|
||
interdit.
|
||
|
||
> **Et la queue de LISTE est ici plus fidèle au principe que la queue de domaine**, ce qui règle
|
||
> la tension au lieu de l'arbitrer : `max(domaine) ≤ max(liste)` toujours, donc la queue de liste
|
||
> est **toujours au moins aussi reculée**. Quand le domaine occupe déjà la fin de la liste — le
|
||
> cas ordinaire d'un domaine qui ne grandit que par détection — les deux coïncident. Quand ils
|
||
> divergent, la queue de liste place la nouvelle charge **derrière tout le monde** au lieu de
|
||
> l'intercaler devant une charge classée : c'est exactement « remplir un trou sans décider ».
|
||
|
||
**LM-1209-a — L'égalité de rang doit être IMPOSSIBLE, et le contrat doit la garantir plutôt que
|
||
la supposer.**
|
||
|
||
`1 + max` produit un rang libre par construction, y compris sur une configuration héritée qui
|
||
contiendrait déjà des doublons. Mais la garantie ne peut pas reposer sur le seul chemin
|
||
d'insertion : une écriture cliente doit être **refusée** si elle pose deux charges au même rang.
|
||
|
||
> ⚠️ **Ce que l'égalité coûte n'est PAS l'indéterminisme** — cette lecture est périmée depuis
|
||
> 3g-2, et s'y fier ferait chercher le défaut au mauvais endroit. Le waterfall trie sur le
|
||
> couple **`(rang, identifiant)`** (`rulebasedscheduler.cpp`), un ordre **total et reproductible**
|
||
> ajouté précisément parce que deux bornes à égalité rendaient irreproductible laquelle
|
||
> démarrait. Trois charges au rang 1 sont donc servies dans un ordre **stable** — mais **arbitraire
|
||
> et non réglable** : il obéit à des identifiants, c'est-à-dire à personne. C'est cela que
|
||
> l'unicité corrige, et c'est une exigence de gouvernabilité, pas de déterminisme.
|
||
|
||
**LM-1209-a-bis — Une vérification qui ÉCARTE et une vérification qui REFUSE ne peuvent pas
|
||
être la même fonction.** *(Règle générale, posée le 2026-08-28 sur un piège trouvé en
|
||
implantant LM-1209-a. Elle vaut au-delà des rangs.)*
|
||
|
||
Le moteur porte deux familles de contrôles, et leur seul geste correct est **opposé** :
|
||
|
||
| Famille | Ce qu'elle nomme | Geste correct | Où |
|
||
|---|---|---|---|
|
||
| **Sûreté d'ensemble** | l'ensemble est **dangereux** — deux charges commandent le même organe, deux commandes contradictoires | **ÉCARTER** l'entrée fautive et continuer | chargement **et** écriture |
|
||
| **Bonne forme** | l'ensemble est **mal formé** — deux charges au même rang | **REFUSER** l'écriture entière | écriture **seulement** |
|
||
|
||
**Les fusionner convertit une règle de contrat en perte de configuration silencieuse.**
|
||
`LoadConfigStore::load()` applique la vérification d'ensemble en écartant les entrées fautives —
|
||
« tolérance ascendante », et c'est le bon geste pour un conflit d'organe : une charge absente ne
|
||
commande rien, deux adaptateurs sur un relais se contrediraient. Y ajouter l'unicité de rang
|
||
ferait qu'une configuration héritée à rangs doublés **perdrait des charges au démarrage** :
|
||
disparues de la configuration *et* de l'arbitrage, **sans `applySafeState`** — donc laissées dans
|
||
leur dernier état commandé, exactement le trou d'ECS-413 rouvert par une autre porte. Et sur la
|
||
foi d'une règle resserrée *après* que la configuration a été écrite.
|
||
|
||
**Le comportement de MIGRATION qui en découle** : au chargement, un doublon hérité est **signalé
|
||
bruyamment et CONSERVÉ** ; la première écriture cliente le répare. Rien n'est perdu entre-temps,
|
||
et l'installation continue de fonctionner dans l'ordre stable — quoique non réglable — qu'elle
|
||
avait déjà.
|
||
|
||
> **Le test à écrire est celui de la moitié qui ne se voit pas.** Qu'une écriture mal formée soit
|
||
> refusée se remarque tout de suite ; qu'un chargement conserve ce qu'il aurait pu écarter ne se
|
||
> remarque jamais — sauf le jour où une box perd trois charges au redémarrage.
|
||
|
||
**LM-1209-b — Insérer ne renumérote personne.** Aucun autre rang ne bouge. C'est le pendant
|
||
exact de la suppression côté app, qui laisse les rangs en place : **un geste de configuration ne
|
||
doit pas devenir un geste d'arbitrage.** Renuméroter pour « faire de la place » modifierait
|
||
l'ordre de service de charges auxquelles personne n'a touché — et le ferait sans qu'aucune
|
||
décision d'exploitation ait été prise.
|
||
|
||
**LM-1209-e — Le rattrapage est CYCLIQUE : la condition est « cette charge n'a pas d'entrée »,
|
||
jamais « l'adaptateur vient de naître ».** *(Défaut trouvé sur `.75` le 2026-08-29, corrigé le
|
||
même jour.)*
|
||
|
||
Le provisionnement vivait dans la branche « l'adaptateur vient d'être créé ». Or **l'adaptateur
|
||
est construit depuis le Thing** : il survit à la suppression de l'entrée de configuration. Une
|
||
entrée supprimée depuis l'app n'était donc **jamais recréée** — mesuré, rien en 140 s.
|
||
|
||
**Ce que cela produisait est le trou de 3g-2, rouvert par le côté client** : la charge restait
|
||
**arbitrée** mais **absente de `GetLoadConfig`**, à un rang de queue non réglable. Elle
|
||
consommait du budget sans que personne puisse la classer, et rien ne le disait. Le seul
|
||
rattrapage était un redémarrage du service.
|
||
|
||
> **La leçon dépasse ce défaut** : un rattrapage dont la condition est un **événement**
|
||
> (« à la naissance de… ») ne rattrape rien, puisqu'il ne se rejoue pas. Une condition d'**état**
|
||
> (« cette charge n'a pas d'entrée ») se réévalue, donc elle répare. C'est la même distinction
|
||
> qu'entre ECS-411 — relire l'état réel des relais — et l'hypothèse de départ qu'il a remplacée.
|
||
|
||
**Une entrée recréée revient en `rankOrigin: "auto"`**, et c'est juste : personne ne l'a classée,
|
||
son rang est de nouveau un défaut. Le rang que l'installateur avait posé est **perdu avec
|
||
l'entrée** — c'est le prix de la suppression, et c'est précisément pourquoi la marque doit se
|
||
voir : la charge réapparaît **en queue, avec son badge**, plutôt que de revenir en silence à une
|
||
place que personne n'a choisie.
|
||
|
||
> ⚠️ **Corollaire d'exploitation** : supprimer l'entrée d'une charge dont le Thing vit n'est
|
||
> **pas** un moyen de la retirer de l'arbitrage — elle revient au cycle suivant, déclassée.
|
||
> `enabled: false` est la voie, et lui conserve son rang.
|
||
>
|
||
> **Et le REFUS de provisionnement ne s'annonce qu'une fois par charge.** Le rattrapage étant
|
||
> cyclique, répéter l'avertissement en produirait un par minute et pour toujours — la règle est
|
||
> déjà posée pour les charges sans décision (`buildTelemetry()`) : avertir à chaque cycle noie
|
||
> le vrai signal. Le drapeau tombe au premier succès, donc un défaut qui se résout se redit s'il
|
||
> revient.
|
||
|
||
**LM-1209-d — Le rang est un ordre TOTAL ; les domaines ne le partitionnent pas.** *(Point
|
||
ouvert soulevé par l'agent app, tranché le 2026-08-29.)*
|
||
|
||
L'insertion en queue entrelace les domaines — `ecs, ev, ev, heating, ev`. Faut-il que
|
||
l'insertion respecte la **contiguïté** d'un domaine, ou que le regroupement automatique côté
|
||
client cesse d'être automatique ? **La seconde.**
|
||
|
||
**La contiguïté n'a jamais été un invariant, et rien dans le moteur n'en a besoin** : le
|
||
waterfall trie sur le **rang seul**, et `domain` est une métadonnée d'intention que l'arbitre ne
|
||
lit pas (LM-1101 · §3). La contiguïté est une convention d'**affichage**, inventée par le client.
|
||
|
||
**Et l'exiger du moteur serait CONTRADICTOIRE avec la raison même de LM-1209**, pas seulement
|
||
avec sa condition « aucun autre rang ne bouge ». Insérer une charge `heating` en respectant la
|
||
contiguïté obligerait à décaler toutes les charges `ev` situées après — c'est-à-dire **une
|
||
détection qui réordonne une installation qu'un humain a classée**, exactement ce que LM-1209
|
||
interdit. On ne peut pas invoquer LM-1209 pour la queue et l'enfreindre pour le regroupement.
|
||
|
||
> **Le même raisonnement s'applique au CLIENT, et c'est le vrai point.** Un regroupement par
|
||
> domaine appliqué **à l'enregistrement** est aussi une machine qui réordonne ce qu'un humain a
|
||
> décidé — au pire moment, et sans que personne l'ait demandé. Il défait l'entrelacement
|
||
> délibéré (« l'ECS après la borne 1 mais avant la borne 2 » est un arbitrage légitime, et le
|
||
> §10 en produira d'autres), et il remonterait au rang 3 une borne que le moteur vient de ranger
|
||
> en queue.
|
||
>
|
||
> **Grouper à l'AFFICHAGE : oui. Grouper à l'ÉCRITURE : non.** Si le regroupement a de la
|
||
> valeur, il doit être un **geste explicite** de l'installateur — visible, réversible —, jamais
|
||
> une normalisation au moment de sauver.
|
||
>
|
||
> **Conséquence sur la note d'entrelacement** : elle ne doit pas s'énoncer comme un
|
||
> avertissement. Une liste entrelacée n'a rien d'incohérent ; l'annoncer comme un défaut pousse
|
||
> à « réparer » un ordre que personne n'a cassé. Au plus une **proposition**.
|
||
|
||
**LM-1209-c — Une entrée créée d'office DOIT se dire.** Une charge qui prend une place dans
|
||
l'ordre de service sans que rien ne le signale est un **changement d'arbitrage silencieux**. Le
|
||
journal l'annonce déjà ; cela ne suffit pas — l'app ne lit pas le journal, et c'est elle qui
|
||
montre le classement. La charge utile doit porter de quoi distinguer **« ce rang est un défaut »**
|
||
de **« ce rang est un choix »**.
|
||
|
||
C'est la même exigence que C2 de LM-1009 et que LM-1106 : **le régime se publie, il ne se déduit
|
||
pas.** Un client ne peut pas deviner qu'un rang n'a jamais été choisi — rien dans le nombre ne le
|
||
dit.
|
||
|
||
**La forme retenue : `o:rankOrigin`, énumération fermée `"auto" | "user"`, absente par défaut.**
|
||
|
||
- **`"auto"`** — l'entrée a été créée d'office et **son rang n'a jamais été choisi**. Seul le
|
||
moteur l'écrit.
|
||
- **`"user"`** — le rang est **assumé**. C'est la voie par laquelle un client lève la marque
|
||
**sans déplacer la charge** : « j'ai vu ce défaut et il me va ».
|
||
- **absente — rien n'est affirmé.** Ni « choisi », ni « par défaut ».
|
||
|
||
> **Une énumération et non un booléen, et la raison est le troisième état.** `rankIsDefault:
|
||
> false` *affirmerait* que le rang a été choisi — or c'est précisément ce qu'on ignore d'une
|
||
> configuration héritée. **L'absence doit porter le « on ne sait pas »**, et un booléen ne sait
|
||
> pas se taire. C'est le même motif de conception que `measurement.source` (§11), et il mérite
|
||
> d'être nommé comme tel plutôt que redécidé au cas par cas : **publier le régime, et ne jamais
|
||
> laisser un silence passer pour une affirmation.**
|
||
|
||
**Trois règles de transition, et la deuxième est celle qu'on se serait trompé à écrire** :
|
||
|
||
1. **Rang CHANGÉ pour cet id → la marque est effacée.** Quelqu'un a classé la charge. Effacée
|
||
vers le **vide**, pas vers `"user"` : le moteur constate un déplacement, il ne sait pas si un
|
||
humain ou un script l'a voulu — ne rien affirmer est le minimum honnête.
|
||
2. **Rang INCHANGÉ → la marque est CONSERVÉE, même si le client l'omet.** `SetLoadConfig`
|
||
remplace en bloc : réordonner une **autre** charge renvoie forcément l'entrée de celle-ci, et
|
||
l'effacer à cette occasion perdrait une marque encore vraie — la charge n'a toujours été
|
||
classée par personne. **L'effacement porte sur le RANG, jamais sur la réception d'une
|
||
écriture.**
|
||
3. **`"auto"` reçu là où le persisté ne l'a pas → refus.** Un client ne fabrique pas une
|
||
affirmation sur un geste du moteur. L'aller-retour **verbatim** d'une entrée qui la porte déjà
|
||
reste licite — sans cette tolérance, la neutralité `Get` → `Set` serait rompue et tout client
|
||
renvoyant ce qu'il a lu se verrait refusé.
|
||
|
||
**LM-1207 — Ne PAS ajouter de `priority` à `ChargingInfo`.** Ce serait un **second système de
|
||
priorité**, et il serait immédiatement insuffisant : il ne saurait pas exprimer
|
||
**« VE1 > ECS > VE2 »**, puisqu'il ne classerait que les bornes entre elles. Deux classements
|
||
séparés qui ne peuvent pas s'interclasser ne sont pas deux fonctionnalités, c'est un défaut.
|
||
|
||
Le rang du VE doit vivre **là où vivent tous les rangs** : dans `LoadConfig.priority`. C'est
|
||
une conséquence directe de 3g, et une raison de plus de ne pas le différer.
|
||
|
||
---
|
||
|
||
## §13 — Ordre d'implémentation recommandé
|
||
|
||
**Ce n'est pas l'ordre dans lequel les trois manques ont été énoncés**, et la raison tient à ce
|
||
que chacun coûte et rapporte.
|
||
|
||
**1. §11 — compteur et sonde.** *Le moins cher, et il conditionne le reste.*
|
||
|
||
Purement **additif** : deux champs optionnels, aucune décision d'arbitrage modifiée, aucune
|
||
migration de configuration. Le risque de régression est proche de zéro.
|
||
|
||
Mais surtout : **la sonde constitue l'historique sans lequel les seuils du §10 ne pourront pas
|
||
être réglés.** Un ballon révèle sa constante de temps en quelques jours, une PAC en une saison.
|
||
Chaque semaine où ce champ n'existe pas est une semaine de données qu'on n'aura jamais.
|
||
Le différer ne coûte pas du temps de développement — il coûte du **temps calendaire**.
|
||
|
||
**2. §12 — le plancher.** *Contenu, et il débloque 3g.*
|
||
|
||
Le plancher est **dérivable** pour deux mécanismes sur trois : seul `etmvariableload` demande un
|
||
champ. Le vrai travail est la règle d'allocation LM-1203 (« sous le plancher → 0, et on
|
||
transmet »), qui est locale au waterfall et se teste sans matériel.
|
||
|
||
Il débloque 3g, qui débloque à son tour l'unification du rang (LM-1207). Trois choses attendent
|
||
derrière une seule.
|
||
|
||
**3. §10 — éco / confort.** *Le plus gros, et le seul qui franchisse une frontière.*
|
||
|
||
À faire en dernier, pour trois raisons :
|
||
|
||
- il **change le waterfall lui-même**, alors que les deux précédents l'entourent ;
|
||
- il fait de l'arbitre un **acheteur**, ce qui ouvre le prix, l'heure et le signal tarifaire ;
|
||
- **il est indissociable du délestage général** (LM-1006-1, désormais **défini** — et il est à
|
||
écrire comme un plafond de soutirage à trois sources, pas comme la protection de surcharge),
|
||
qui n'existe pas encore : la
|
||
protection de surcharge ne déleste que les bornes. Livrer les planchers éco sans lui, c'est
|
||
livrer un mécanisme capable de faire disjoncter une installation un matin sans soleil.
|
||
|
||
> **PRÉCONDITION DE LIVRAISON — pas une recommandation.** Le §10 **ne part pas** sans
|
||
> l'extension du délestage au waterfall. Même lot, ou avant. **Jamais après.**
|
||
>
|
||
> Aujourd'hui la protection de surcharge ne déleste **que les bornes de recharge**
|
||
> (`m_evChargers` dans `SmartChargingManager::verifyOverloadProtection`). Des planchers éco qui
|
||
> s'additionnent un matin sans soleil sur un branchement 6 kVA feraient **disjoncter
|
||
> l'installation** — pas dégrader le confort : couper le courant, chez un client.
|
||
>
|
||
> Écrit comme condition et non comme conseil parce que c'est exactement le genre de dette
|
||
> qu'on ne rembourse jamais : elle ne se manifeste qu'aux conditions extrêmes, donc « ça marche
|
||
> chez tout le monde » jusqu'au jour où ça ne marche chez personne. *(Décision Patrick,
|
||
> 2026-08-26.)*
|
||
|
||
**Ce qui peut se faire en parallèle, sans rien attendre** : la publication en télémétrie de la
|
||
sonde et du diagnostic de mesure (§11), puisqu'elle n'a aucun effet sur les décisions.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## §14 — Le stockage est-il une charge arbitrable ?
|
||
|
||
> **Statut : réponse de conception. Ne déclenche aucun travail** — elle dit sous quelle forme
|
||
> l'étape **3f** (déjà déférée : *« BatteryAdapter — constraints + charge réseau plafonnée +
|
||
> waterfall grid-funding »*) devra se faire, et lève les quatre points que l'agent app a nommés.
|
||
|
||
**La demande, telle que Patrick l'a formulée** : *« si je veux que l'énergie aille d'abord dans
|
||
l'eau chaude, et seulement si l'eau est chaude je commence à charger la batterie, je fais
|
||
comment ? »*
|
||
|
||
**Réponse courte : aujourd'hui il ne peut pas, et la réponse est OUI — le stockage doit devenir
|
||
une charge du waterfall.** Ce n'est pas une porte à ouvrir : elle est déjà encadrée, et depuis le
|
||
début.
|
||
|
||
---
|
||
|
||
### LM-1400 — Les quatre constats sont exacts, vérifiés indépendamment
|
||
|
||
| Constat | Vérifié |
|
||
|---|---|
|
||
| `batteryLevelConsideration` donne la priorité à la BATTERIE, pas aux charges | oui — `rulebasedscheduler.cpp:125-139`, sous le seuil le budget est **annulé** |
|
||
| La batterie n'est dans aucun waterfall | oui — aucune entrée `evcharger`/`battery` dans `GetLoadConfig` ; quatre charges sur `.75` |
|
||
| Le moteur ne la commande nulle part | oui — `enableCharging`, `enableDischarging`, `chargingRate`, `dischargingRate`, `gridCharging` : **0 occurrence** dans `energyplugin/` |
|
||
| Le levier existe | oui — la classe `sunspecStorage` du banc déclare **les cinq actions**, et l'énumération `domain` accepte déjà `battery` sans que rien n'en fasse quoi que ce soit |
|
||
|
||
**Et le contrat l'attendait.** `OPTIMIZER_PROTOCOL.md` décrit déjà la batterie comme une charge de
|
||
`loads[]` (`"adapter":"battery"`), avec **deux kinds** dont un lui est réservé :
|
||
|
||
| kind | ce qu'il exprime | déjà déclaré |
|
||
|---|---|---|
|
||
| `setpoint` + `source: "grid"` | charger la batterie, éventuellement **depuis le réseau** | `LoadAction::Source { Solar, GridSource }` |
|
||
| `constraint` | `charge`/`discharge` : `allow` \| `forbid` | `LoadAction::Permission { Allow, Forbid }` |
|
||
|
||
`SurplusBattery` porte déjà `reserveSocPercent`, `maxChargeW`, `maxDischargeW` — trois champs
|
||
qu'aucun code ne lit aujourd'hui. **Le modèle a été conçu pour ça et n'a jamais été câblé.**
|
||
`domain: "battery"` accepté sans effet n'est donc pas une question effleurée : c'est le reliquat
|
||
visible d'une place réservée.
|
||
|
||
---
|
||
|
||
### LM-1401 — (a) Le double commandeur : la réponse est MESURABLE, pas à deviner
|
||
|
||
C'est la bonne question, et c'est la leçon de 3g-2 correctement appliquée : **avant d'ajouter un
|
||
commandeur, savoir qui commande déjà**. Mais ici, contrairement aux bornes, **le détecteur existe
|
||
déjà et il est publié**.
|
||
|
||
Le plugin SunSpec **écrit, relit, et compare** (`integrationpluginsunspec.cpp:703-726`), et son
|
||
commentaire nomme exactement le cas redouté :
|
||
|
||
> *« An inverter may accept a write and then ignore it without raising a Modbus exception —
|
||
> control not enabled on the device, grid charging not allowed, an IP restriction, or **another
|
||
> controller overwriting the setpoints**. An unverified setpoint is an unapplied setpoint. »*
|
||
|
||
Le résultat sort dans l'état **`controlHealthy`**, déjà déclaré par la classe du banc.
|
||
|
||
**Conséquence pour 3f, et elle est forte** : la question « l'onduleur obéit-il ? » ne se tranche
|
||
pas sur le papier — **elle s'observe**. Trois exigences en découlent :
|
||
|
||
1. **Mesurer avant de commander.** Une consigne d'essai, `controlHealthy` relu. Si l'onduleur
|
||
re-décide, il le dira. C'est exactement ce qui manquait aux bornes avant 3g-2, et qui a coûté
|
||
un lot entier.
|
||
2. **`controlHealthy == false` doit sortir de l'arbitrage**, comme `available == false` le fait
|
||
déjà pour une charge en défaut (ECS-410) : une batterie dont les consignes ne prennent pas
|
||
n'est pas pilotable, et lui allouer du budget serait le trou de `pluggedIn` à nouveau.
|
||
3. **Le motif doit le dire.** Une batterie non servie parce que l'onduleur reprend la main est un
|
||
diagnostic d'installation, pas un manque de surplus. Deux causes, deux gestes.
|
||
|
||
---
|
||
|
||
### LM-1402 — (b) `batteryLevelConsideration` cesse d'être un rang et redevient une RÉSERVE
|
||
|
||
Les deux mécanismes ne sont pas concurrents **par nature** — ils le sont par accident
|
||
d'implémentation. Ce sont deux objets différents que le code actuel confond :
|
||
|
||
| | ce que c'est | ce que le code en fait aujourd'hui |
|
||
|---|---|---|
|
||
| **rang** (`priority`) | l'ordre de service : qui reçoit le surplus en premier | n'existe pas pour la batterie |
|
||
| **réserve** (`batteryLevelConsideration`) | un plancher : sous ce niveau, on protège le stockage | **agit comme un rang absolu** — annule tout le budget des autres |
|
||
|
||
**Position : quand la batterie devient une charge rangée, le seuil PERD son rôle de priorité et
|
||
ne garde que son rôle de plancher.** « La batterie d'abord » s'exprime alors par le rang — et
|
||
« l'eau chaude d'abord » aussi, dans la même liste, par le même geste.
|
||
|
||
Le précédent existe et il va dans ce sens : le défaut d'usine est passé de `0,9` à `0,2` en
|
||
3g-1, précisément parce que **`0,9` confondait une cible et une réserve** (LM-1203-b). Ce lot
|
||
finit ce mouvement.
|
||
|
||
> **Ne pas garder les deux rôles.** Deux réglages dont l'un défait l'autre en silence est
|
||
> exactement ce que LM-1207 refuse pour les priorités : deux classements qui ne s'interclassent
|
||
> pas ne sont pas deux fonctionnalités, c'est un défaut. Et l'écran ne saurait pas le présenter.
|
||
|
||
---
|
||
|
||
### LM-1403 — (c) La sûreté : deux gestes très différents sous un seul mot
|
||
|
||
« Le rang peut-il vider une batterie ? » — la question se scinde, et la scission désamorce la
|
||
moitié de l'inquiétude :
|
||
|
||
| Geste | Effet sur le SOC | Dangereux ? |
|
||
|---|---|---|
|
||
| **Ne pas la charger** (servir l'ECS d'abord) | le SOC **ne monte pas** | **non** — réversible, le soleil revient. C'est la demande de Patrick, et elle ne vide rien |
|
||
| **La décharger pour servir une charge** | le SOC **descend** | **oui** — et c'est le seul cas à border |
|
||
|
||
Or le second est **déjà refusé aujourd'hui**, sans rapport avec le rang
|
||
(`rulebasedscheduler.cpp:152-156`) :
|
||
|
||
```cpp
|
||
if (ctx.battery.powerW < 0) {
|
||
// Décharge : ne pas vider la batterie pour alimenter une charge pilotée, quel que
|
||
// soit le niveau. Ce qu'elle fournit n'est pas du surplus.
|
||
remainingSurplusW += ctx.battery.powerW; // powerW < 0 → retranche
|
||
}
|
||
```
|
||
|
||
**Position** : le rang gouverne uniquement l'**ordre de charge**. Trois planchers restent
|
||
intouchables et ne se règlent pas par glissement — `batteryCritical`, `reserveSocPercent` (déjà
|
||
au contrat, jamais lu), et l'interdiction ci-dessus. **Un rang ne fait jamais descendre un SOC ;
|
||
il décide seulement de ce qui le fait monter.** Le glissement demandé est donc sûr par
|
||
construction, et c'est ce qui permet de l'offrir sans écran d'avertissement.
|
||
|
||
---
|
||
|
||
### LM-1404 — (d) Le signe : l'inquiétude tombe, et pour une raison précise
|
||
|
||
La crainte est légitime après `EV_GRID_START`, mais elle ne s'applique pas ici. **Charger une
|
||
batterie, c'est consommer** — au compteur, c'est indistinguable d'un chauffe-eau qui chauffe.
|
||
|
||
| | `allocatedW` veut dire | signe au bilan |
|
||
|---|---|---|
|
||
| ECS | consommer 2 000 W | **−** au surplus |
|
||
| Borne | consommer 2 000 W | **−** au surplus |
|
||
| **Batterie en charge** | **stocker** 2 000 W | **−** au surplus — **identique** |
|
||
|
||
`allocatedW` d'une batterie qui charge se réconcilie donc avec `budget.allocatedW` **sans aucun
|
||
traitement particulier** : c'est une charge comme les autres, et le waterfall n'a pas à savoir ce
|
||
qu'elle fait de l'énergie.
|
||
|
||
**Le sens opposé n'apparaît qu'à la DÉCHARGE — et une décharge n'est pas une allocation.** C'est
|
||
une *source*, elle appartient au revenu du budget, pas à sa dépense. Le code le traite déjà ainsi
|
||
(LM-1403). **Les mélanger serait le vrai défaut** : une décharge comptée en `allocatedW` négatif
|
||
rendrait faux l'invariant publié `surplus + recrédit − alloué == restant`.
|
||
|
||
> **Règle qui en sort** : `allocatedW` d'une batterie est **toujours ≥ 0**. Ce qu'elle rend au
|
||
> réseau ou à la maison n'est jamais une allocation.
|
||
|
||
---
|
||
|
||
### LM-1405 — Ce qui reste à décider, et que cette note ne tranche pas
|
||
|
||
- **La charge réseau plafonnée** (`setpoint` + `source: "grid"`) : acheter pour stocker est un
|
||
franchissement de frontière, le même que le §10. Il doit suivre les mêmes règles — plafond de
|
||
soutirage de LM-1006-1 compris. **Hors de ce qui répond à la demande de Patrick** : servir
|
||
l'ECS avant la batterie ne demande aucun achat.
|
||
- **L'ordre relatif par défaut.** Une installation existante a aujourd'hui « batterie d'abord »
|
||
par le seuil. Lui donner un rang change son comportement : quel rang par défaut, et
|
||
faut-il migrer les installations en service ? Même famille que le rang par défaut des bornes,
|
||
et même risque — un ordre qui n'est pas un choix mais un artefact (`RELEVE_3g2.md` §3.2).
|
||
- **La forme de l'entrée `LoadConfig`.** `adapter: "battery"`, sur le modèle de `evcharger`
|
||
(rang seul, limites lues sur le Thing) — à confirmer à l'écriture de 3f.
|
||
|
||
### LM-1406 — Ce qu'il ne faut PAS faire en attendant
|
||
|
||
**Une règle `Rules.*` coupant `enableCharging` fonctionnerait, et elle est à écarter.** L'agent
|
||
app l'a écartée de lui-même, pour la bonne raison : ce serait un **second commandeur sans
|
||
arbitre** — précisément le défaut que 3g-2 vient de supprimer sur les bornes, réintroduit par la
|
||
porte de l'app. Et il serait pire ici : sur les bornes, le second commandeur était au moins
|
||
*dans* le moteur, donc observable au journal.
|
||
|
||
**« Pas réglable aujourd'hui » est la bonne réponse à donner à l'utilisateur**, et elle vaut
|
||
mieux qu'un réglage dont personne ne garantit l'effet.
|