diff --git a/docs/INTERFACE_etmvariableload.md b/docs/INTERFACE_etmvariableload.md index 8dc338c..d6a0893 100644 --- a/docs/INTERFACE_etmvariableload.md +++ b/docs/INTERFACE_etmvariableload.md @@ -155,6 +155,59 @@ Règles : le discriminant quand cette couche arrivera. L'énumération de ce spec (§2 : `Ecs`/`Hvac`/`Ev`/`SmartHome`) est une intention plus ancienne ; c'est celle ci-dessus qui est implémentée. + - ⚠️ **`domain` n'a AUCUNE force normative aujourd'hui, et n'en aura pas à court terme.** + Il groupe l'ordre de service, rien d'autre. **Ne construire aucune règle dessus** — ni + côté app, ni côté moteur. Ce que `heating` et `hvac` désignent exactement reste ouvert et + ne se saura qu'en dessinant la PAC communicante. + +### 4-bis. Ce qui fait une charge — **un canal de commande, une charge** + +> **Règle LM-201** (`specs/spec_loadmodel.md` §2). Elle décide de ce que l'écran +> « Configurer » doit **créer**, et c'est la question que l'app pose en premier. + +**Une charge existe quand il y a un canal de commande distinct pour un service.** Le domaine +ne décrit pas la **machine**, il décrit **ce qu'on arbitre** — une PAC air/eau fait chauffage, +rafraîchissement et ECS, et cela ne fait pas trois charges. + +| Installation | Ce que l'app doit créer | Pourquoi | +|---|---|---| +| PAC chauffage + ECS, **sans ballon séparé** | **UNE** charge (`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 — ce qui change est la saison, pas la configuration | + +- **Ne pas créer de charge ECS sur une PAC sans ballon séparé.** La répartition entre chauffage + et ECS appartient à la **régulation de la PAC** ; il n'y a rien à arbitrer indépendamment. +- **Deux charges ne peuvent pas revendiquer le même Thing.** `SetLoadConfig` **rejette en + bloc** (LM-302-b / ECS-110-b). Ce n'est pas une limite technique : deux charges sur un canal + unique recevraient deux consignes pour un seul organe, et le budget compterait la même + puissance deux fois. + +**Les thermostats et les zones ne sont PAS des charges (LM-202).** Un thermostat ouvre une +**vanne** ; il ne porte pas la consommation, qui reste à la machine. Les déclarer produirait +des entrées à **0 W** dans le waterfall pendant que la PAC est comptée ailleurs. Zones et +thermostats sont de la **configuration de confort** — ils disent **où va la chaleur**, pas +**combien on en achète**. Ils vivent dans leur écran et leur namespace, **jamais** dans +`LoadConfig`. + +### 4-ter. Mécanismes recevables selon l'organe (LM-200) + +**Une PAC se pilote de deux façons, et de deux seulement** : `sg-ready` (deux contacts **secs** +de signalisation, 4 états normés) ou **communicante** via un plugin nymea — ce dernier mécanisme +**n'existe pas encore**. + +**Ni `relay-router`, ni `etmvariableload`** : le premier couperait l'**alimentation** du +compresseur et lui retirerait son autorité sur son propre cycle (dégivrage, retour d'huile, +temporisation de redémarrage) ; le second suppose un cadran en watts qu'un compresseur n'est pas. + +> ⚠️ **Piège d'UI : « ma PAC est pilotée par deux relais, donc `relay-router` ».** Non. Les +> contacts SG-Ready **sont** des things `power` — au banc `.75`, K1/K2 en sont deux. Mais ils +> **signalent**, ils ne **portent** pas la charge. Voir des relais dans une configuration ne dit +> **rien** du mécanisme qui convient. Le picker de relais de §10 sert les deux mécanismes. + +Le moteur **n'interdit pas** ces combinaisons — `domain` est facultatif et sans force normative, +un refus serait contournable en effaçant le champ. **C'est donc l'app qui guide au moment du +choix**, et c'est le seul endroit où le guidage est fiable : elle sait quel appareil +l'installateur est en train de déclarer, le moteur ne le sait pas. --- @@ -279,3 +332,14 @@ toucher à l'écran Configurer, sinon il code contre une frontière périmée (r experience-plugin) ; un relais = thing `power` nu ; `LoadConfig` du cas ECS devient une **liste de relais** (`relays[]` + `minOnS`/`minOffS`) ; `powerLevels` **dérivés** ; cas continu (`EtmVariableLoadAdapter`) inchangé. **Impact app §10.** +- **lot B-bis (la PAC entre en configuration)** : plus **aucun** adaptateur codé en dur — + `loads[]` de `GetLoadTelemetry` est **inclus** dans `GetLoadConfig`, par construction ; la + règle de détection « un `loadId` sans équivalent dans `GetLoadConfig` » est **retirée** + d'`INTERFACE.md` et aucun `configurable: false` n'a été ajouté. `pac-terrain` est devenue une + `LoadConfig` `sg-ready` ordinaire. +- **§4-bis / §4-ter ajoutés** : **LM-201** — *un canal de commande, une charge* — qui décide de + ce que l'écran « Configurer » doit **créer** (une PAC sans ballon séparé = **une** charge, pas + deux) ; **LM-202** — thermostats et zones **ne sont pas** des charges ; **LM-200** — les + mécanismes recevables selon l'organe, et le piège « pilotée par deux relais donc + `relay-router` ». `domain` est explicitement **sans force normative** : c'est l'**app** qui + guide au moment du choix, le moteur ne le peut pas. diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index af9cbb8..c5f37fc 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -1,12 +1,19 @@ # SPEC — Modèle de charges (domaine × mécanisme) -Version : 0.1.3 +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 @@ -136,12 +143,75 @@ d'intégration, invisible d'ici. À renommer quand il sera implémenté ; à ne 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. +**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. --- @@ -373,28 +443,23 @@ distinct, pas comme une variante. ## §8 — Points ouverts 1. `typeAppareil` — quelle énumération exacte, et jusqu'où elle affine le - domaine. + 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. **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.** +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.)* ---