Décisions de Patrick, écrites dans la spec et portées à l'agent app qui avait remonté la question. 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 lit comme une lecture du véhicule. Le pourcentage reste dans la saisie de l'intention, il sort de l'affichage de l'avancement. D2 — obligation par SESSION d'abord. Elle ne demande que de lire un champ déjà publié, c'est l'usage réel (on branche le soir pour partir le matin), et surtout la remise à zéro de sessionEnergy au branchement CESSE d'être une réserve : pour une obligation de session, c'est la bonne sémantique. Le texte est réécrit en conséquence — le problème disparaît au lieu d'être contourné. L'obligation quotidienne devient un lot à part, dépendant de l'historique du §11. 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. Précédent de nommage déjà dans ce document (estimatedPowerW), et même refus que le currentPowerStorage à zéro du relevé 3g-1 : mieux vaut « estimé », ou rien, qu'un chiffre crédible et faux. Les deux réserves deviennent des CONDITIONS DE LIVRAISON, pas des notes — ce sont les deux façons dont le §10 peut mentir sans erreur visible : C1 — une borne sans sessionEnergy dégrade VISIBLEMENT. Mesuré : wallboxNoMeter et l'une des deux classes wallbox ne l'ont pas. Une échéance qui cesse silencieusement d'être mesurable est pire qu'une échéance absente, parce qu'elle continue de s'afficher. Et « pas mesurable » n'est pas « rien livré ». 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. La distinction des deux régimes est conservée telle quelle : une obligation en énergie peut annoncer son atteinte, une cible en pourcentage publie une intention et un avancement estimé — jamais « tenue ». Le brief app gagne un cinquième point : c'est celui qui change un écran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
1082 lines
63 KiB
Markdown
1082 lines
63 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.** Vrai du disjoncteur **par construction**. Ce sera
|
||
une **obligation légale** pour le §14a : une coupure totale y est interdite, mais le plafond,
|
||
lui, doit tenir — y compris quand le moteur ne décide plus.
|
||
|
||
> C'est une contrainte sur `degradedMode`, et elle n'est pas celle qu'on aurait devinée. Le
|
||
> repli L2 d'aujourd'hui est conçu pour **ne rien initier** (`docs/SAFETY.md` §L2) ; il n'est
|
||
> pas conçu pour **continuer à faire respecter une borne** qu'un tiers a imposée. Autant
|
||
> concevoir le repli là-dessus tout de suite plutôt que de le découvrir en certification.
|
||
|
||
**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.
|
||
|
||
**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.
|
||
|
||
**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.
|
||
|
||
---
|
||
|
||
## §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.
|
||
|
||
**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-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.
|
||
|
||
---
|