317 lines
16 KiB
Markdown
317 lines
16 KiB
Markdown
# SPEC — Modèle de charges (domaine × mécanisme)
|
||
|
||
Version : 0.1.2
|
||
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.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.
|
||
|
||
**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 |
|
||
|
||
Les cases vides ne sont pas interdites, elles ne sont simplement pas prévues.
|
||
|
||
---
|
||
|
||
## §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-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** — Chaque charge utile valide la sienne. Les états invalides doivent
|
||
être inexprimables : pas de registre Modbus dans une configuration relais.
|
||
|
||
**Portée exacte.** L'union discriminée s'applique aux **types C++** et au
|
||
**schéma persisté**. Elle ne s'applique **pas** au schéma JSON-RPC : nymea valide
|
||
les paramètres *avant* le handler, si bien que deux formes exclusives obligent à
|
||
marquer tous les champs spécifiques en optionnel (`"o:"`), faute de quoi l'une
|
||
des deux serait rejetée en amont. La contrainte est déjà documentée dans
|
||
`nymeaenergyjsonhandler.cpp:155-160` pour deux mécanismes ; elle s'aggrave à
|
||
quatre. **Le mur a été touché le 2026-08-10**, exactement où ce paragraphe l'annonçait, et d'une
|
||
façon qu'il n'avait pas anticipée : ce n'est pas seulement que deux formes exclusives
|
||
obligent à marquer les champs `"o:"`, c'est que `pack<LoadConfig>()` sérialise **toutes**
|
||
les propriétés déclarées — une charge `relay-router` sort donc de `GetLoadConfig` avec un
|
||
`sgReady` vide. Exiger une clé **à l'intérieur** d'une charge utile optionnelle casse
|
||
alors le va-et-vient de l'app sur les charges des **autres** mécanismes. Corollaire pour
|
||
tout mécanisme futur : les clés internes d'une charge utile se marquent `"o:"` elles
|
||
aussi. À la frontière RPC, la validation reste donc **à l'exécution**
|
||
(`LoadConfig::isValid()`). Ne pas tenter un schéma RPC strict : c'est un mur
|
||
connu.
|
||
|
||
**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.
|
||
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.
|
||
|
||
---
|
||
|
||
## §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.
|