LM-201 est la règle qui manquait sous LM-200. Une charge existe quand il y a un canal de commande DISTINCT pour un service. Elle repose la question du domaine, que 0.1.3 posait mal : 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 : PAC + ECS sans ballon séparé = UNE charge (heating), pas de charge ecs — le SG-Ready commande la machine entière et la répartition appartient à sa régulation ; ballon séparé = charge distincte, il a son propre canal ; PAC qui fait du froid = toujours une seule charge, ce qui change est la saison, pas la configuration. La règle explique l'exclusivité de validateSet() (LM-302-b / ECS-110-b) : deux charges sur un canal unique se contrediraient, et le budget compterait la même puissance deux fois. Le refus existait avant la règle ; la règle dit enfin pourquoi il est juste. LM-202 : thermostats et zones ne sont pas des charges. Un thermostat ouvre une vanne, il ne porte pas la consommation. Les déclarer produirait des entrées à 0 W dans le waterfall pendant que la PAC est comptée ailleurs. Ils décrivent où va la chaleur, pas combien on en achète — leur écran, leur namespace, jamais LoadConfig. Enforcement LM-200 tranché : avertissement au journal, PAS de refus dans isValid(). Mais l'avertissement n'a pas encore de prédicat — voir le rapport de session. Keyé sur domain seul il crierait sur des installations correctes (heating + relay-router = chauffage résistif ; hvac + relay-router = VMC). Son discriminant est typeAppareil (§8-1), valeur thermodynamique. Spécifié ici, implémenté avec lui. §8-5 : heating vs hvac reste OUVERT et ne se saura qu'en dessinant la PAC communicante, où les consignes ECS et chauffage sont enfin séparées. D'ici là o:domain garde son rôle actuel, sans force normative — ne construire aucune règle dessus. INTERFACE_etmvariableload.md (canonique) : §4-bis et §4-ter ajoutés, côté app. C'est l'app qui guide au moment du choix, et c'est le seul endroit fiable — elle sait quel appareil l'installateur déclare, le moteur ne le sait pas. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
472 lines
26 KiB
Markdown
472 lines
26 KiB
Markdown
# SPEC — Modèle de charges (domaine × mécanisme)
|
||
|
||
Version : 0.1.4
|
||
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.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. **`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.
|