docs(spec): LM-200 — une case vide de la matrice n'est pas toujours une lacune

La PAC ne se pilote qu'en SgReady (contacts SECS de signalisation, 4 états normés) ou en
COMMUNICANT via un plugin nymea. Ni Relay ni Variable : le premier couperait l'alimentation
du compresseur et lui retirerait son autorité sur son propre cycle (dégivrage, retour
d'huile, temporisation) ; le second suppose un cadran en watts qu'un compresseur n'est pas.

LM-101 disait « le domaine ne détermine pas le mécanisme et réciproquement » — vrai du
couplage de CONCEPTION, faux lu comme une liberté de combiner. Précisé.

Piège nommé au passage : les contacts SG-Ready SONT des relais (K1/K2 au banc sont deux
things power), mais ils signalent au lieu de porter la charge. Voir des relais dans une
config ne dit rien du mécanisme qui convient.

Le mécanisme communicant, encore appelé ModbusSetpoint ici, porte un nom trop étroit : ce
qui le définit est qu'un thing nymea porte le modèle de la machine, pas le transport.

§8 point 5 : faire du domaine un discriminant de validation est une décision de contrat,
pas un correctif — trois options pesées, non tranchée.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
This commit is contained in:
Patrick Schurig 2026-08-25 21:31:43 +02:00
parent afaa8c2674
commit 7c4248ca97

View File

@ -1,12 +1,18 @@
# SPEC — Modèle de charges (domaine × mécanisme)
Version : 0.1.2
Version : 0.1.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.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.
@ -37,7 +43,11 @@ document reste une intention.
**É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.
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
@ -83,7 +93,55 @@ SmartHome.
| `Ev` | `Relay` (prise commandée : GreenUp, Witty) · `ModbusSetpoint` (borne) |
| `SmartHome` | `Relay` — voir §7, sémantique différente |
Les cases vides ne sont pas interdites, elles ne sont simplement pas prévues.
**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.** `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()`, et
l'omission du domaine contournerait de toute façon tout contrôle qui s'y
appuierait. Faire du domaine un discriminant de validation est une **décision de
contrat**, pas un correctif — elle est portée en §8.
---
@ -321,6 +379,22 @@ distinct, pas comme une variante.
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. **Le domaine doit-il devenir un discriminant de validation (LM-200) ?** Aujourd'hui
`domain` est une métadonnée optionnelle que l'arbitre ne lit pas ; rien ne refuse
`domain: "hvac"` + `adapter: "relay-router"`. Trois façons de traiter la case interdite,
par coût croissant :
- **la documenter seulement** — l'app guide le choix, le moteur ne juge pas. C'est
l'état actuel, désormais écrit ;
- **refuser dans `isValid()`** — cohérent avec « les états invalides doivent être
inexprimables » (LM-302), mais `domain` cesse d'être une métadonnée : ce serait un
changement de contrat, à annoncer à l'app. Et le contrôle reste **contournable par
omission**, puisque le domaine est facultatif — un refus qu'on esquive en effaçant
un champ protège mal ;
- **avertir sans refuser** — une ligne de journal à la construction de l'adaptateur.
Ne bloque pas une installation atypique, laisse une trace lisible au banc.
Le troisième est le seul qui ne mente ni sur le statut de `domain`, ni sur la force
réelle du contrôle. **À trancher, pas par moi.**
---