Note de conception. Aucun code, aucun champ, aucune entrée au modèle de
charges : elle existe pour que la porte reste ouverte.
Le §14a EnWG 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, avec
une source de commande différente. Il y a donc trois sources pour un seul
mécanisme : le disjoncteur de branchement (physique, permanent), le §14a
(externe, sur ordre, temporaire), et le plancher éco du §10 (implicite, celui
qu'on s'impose en décidant de soutirer).
Ce que la note demande, 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 et ne coûtent rien à tenir dès
l'écriture :
- Le plafond le plus contraignant l'emporte, et on doit pouvoir dire LEQUEL
a mordu. C'est l'exigence qui sépare déjà BELOW_MIN_POWER de
SURPLUS_INSUFFICIENT : un client bridé doit lire « gestionnaire de réseau »
et non « protection de surcharge ». Deux causes, deux gestes — l'une se
subit, l'autre s'attaque en délestant ou en revoyant l'abonnement.
- Le plafond survit à la panne du HEMS. Vrai du disjoncteur par
construction ; ce sera une obligation légale pour le §14a, où la coupure
totale est interdite mais le plafond doit tenir. C'est une contrainte sur
degradedMode, et ce n'est pas celle qu'on aurait devinée : le repli L2
d'aujourd'hui est conçu pour NE RIEN INITIER, pas pour continuer à faire
respecter une borne qu'un tiers a imposée. Autant le concevoir là-dessus
maintenant que le découvrir en certification.
Referme au passage une référence pendante : LM-1006-1 était cité par §13 sans
avoir jamais été défini.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
950 lines
54 KiB
Markdown
950 lines
54 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-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.
|
||
|
||
**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.
|
||
|
||
---
|