From 707526191ae0c3e77de371d3893e65f629cdf57d Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Fri, 28 Aug 2026 14:28:46 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec=20=C2=A711):=20la=20mesure=20d'une=20?= =?UTF-8?q?charge=20se=20lit=20sur=20l'appareil,=20et=20dit=20sa=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constat d'usage de l'agent app, retenu : pour afficher « mesuré » à côté de « alloué », il fallait désigner une borne comme SON PROPRE compteur dans meterThingId. Ce n'est devinable par personne, et une note d'écran qui explique un réglage dit que le défaut est dans le modèle. LM-1105 : quand le Thing piloté porte lui-même l'état de puissance, le moteur le lit sans configuration. La règle porte sur le THING, pas sur le mécanisme — certains powerswitch portent currentPower et RelayRouter sait déjà les sommer ; écrire la règle par mécanisme y ajouterait la table à maintenir à la main que LM-600 refuse. Et le test se pose sur la CLASSE, sur TOUS les Things déclarés : interroger les relais actifs ferait osciller la réponse au palier 0, sur une installation dont rien n'a changé. Le compteur explicite reste prioritaire — répondu oui, et la raison principale n'est pas la précision mais qu'un réglage sans effet est pire que pas de réglage. La priorité porte sur la SOURCE, pas sur la valeur : compteur déclaré injoignable → on retombe sur l'appareil, et la charge utile dit lequel a servi. Le second point de l'agent app décide de la forme, et la vérification l'a rendu plus tranchant qu'il n'était posé : le moteur porte DÉJÀ trois régimes sous un seul nom et jette la distinction. evcharger rend une mesure ; relay-router rend une mesure OU le palier nominal — le booléen `metered` est calculé puis jeté ; sg-ready rend toujours une puissance DÉCLARÉE, jamais mesurée, délibérément. Publier ces quatre lignes sous un même measuredW serait faux pour deux d'entre elles, et un measuredW auto-rempli détruirait au passage la seule information que le champ portait : sa présence signifiait « un compteur a été posé exprès ». D'où LM-1106 : le régime se PUBLIE, il ne se déduit pas d'une valeur — C2 de LM-1009 mot pour mot. measurement {source: meter|device|none, o:powerW}, powerW absent sous none. `none` est un état POSITIF — C1 de LM-1009 : une capacité manquante se dit — et c'est lui qui distingue « pas mesurable » de « compteur en panne ». Sous none, la puissance nominale reste lisible dans mechanism.* : la faire passer pour une mesure produirait un écart identiquement nul à tous les cycles, un « tout concorde » permanent, corollaire LM-104. Ce que la source change au diagnostic, et qui la rend utile plutôt que décorative : device dit « la charge n'obéit pas » (commande et mesure sortent de la même boîte) ; meter ouvre trois hypothèses — charge, compteur, câblage — dont il ne tranche aucune ; none n'autorise rien. L'asymétrie va plus loin : sous device une CONCORDANCE ne prouve rien, plusieurs wallbox renvoyant leur consigne en guise de puissance, alors que sous meter elle est une preuve. device détecte la désobéissance, meter est le seul à pouvoir détecter un mensonge — et c'est ce qui donne à LM-1104 sa raison d'être : meterThingId reste AUTORISÉ sur une charge qui publie déjà, à masquer par défaut, pas à interdire. Ne touche PAS ce que l'arbitre lit : LM-1101 tient, et le recrédit anti-clignotement continue de lire LoadContext::telemetry.currentPowerW, qui vient de l'adaptateur et n'a jamais transité par meterThingId. Ce lot déplace ce qui est PUBLIÉ, pas ce qui décide. Aucun code. Le contrat bouge (measuredW → measurement), et le brief le dit à l'app avec son coût : un écran. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p --- specs/spec_loadmodel.md | 113 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index 3d6c645..87df6b9 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -832,6 +832,11 @@ portant `energymeter` / `smartmeter` ; la sonde parmi celles portant l'interface 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. +> ⚠️ **`meterThingId` n'est plus la SEULE source de mesure — voir LM-1105.** Il l'a été, et cela +> obligeait l'installateur à désigner une borne comme son propre compteur pour qu'elle affiche +> ce qu'elle mesure déjà. Le champ garde tout son sens ; il cesse d'être une corvée de +> configuration là où le moteur peut lire l'appareil directement. + **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. @@ -858,6 +863,9 @@ soupçon. Il se publie comme **diagnostic**, à côté de `faultCode` et sans le > lent et une charge à forte inertie produiraient sinon un diagnostic permanent. Trois cycles > est un point de départ, pas une valeur figée. +> **Et l'écart ne CONCLUT PAS la même chose selon la source de la mesure** — LM-1106. Un écart +> calculé sur une valeur qui n'est pas une mesure ne conclut, lui, rien du tout. + **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 : @@ -879,6 +887,111 @@ compteur de tableau divisionnaire peut couvrir plusieurs charges. `sensorThingId` dans son contrôle d'unicité des Things. Les y ajouter par symétrie avec `relays[]` refuserait des configurations correctes. +**LM-1105 — Le moteur lit l'appareil qu'il pilote. `meterThingId` cesse d'être la seule +source.** *(Constat d'usage de l'agent app, 2026-08-28 ; proposition retenue.)* + +Pour qu'une charge publie ce qu'elle consomme, il fallait la désigner **comme son propre +compteur** dans `meterThingId`. Ce n'est devinable par personne : un installateur voit un bloc +« compteur dédié » sur une borne qui publie déjà `currentPower`, et n'a aucune raison d'y +remettre la borne elle-même. **C'est de la configuration demandée pour compenser ce que le +moteur peut déduire** — et une configuration qu'on documente en note d'écran est le signe que le +défaut est dans le modèle, pas dans l'app. + +**La règle** : quand le Thing piloté **porte lui-même l'état de puissance**, le moteur le lit, +sans configuration. `meterThingId` ne reste nécessaire que là où rien ne mesure. + +> **La règle porte sur le THING, pas sur le mécanisme.** « Les bornes mesurent, les relais +> non » est vrai en pratique et faux en droit : certains `powerswitch` portent `currentPower`, +> et `RelayRouter` sait déjà les sommer. Écrire la règle par mécanisme y ajouterait la table à +> maintenir à la main que LM-600 refuse. Le test est une question posée à la **ThingClass**. +> +> **Et il se pose sur la CLASSE, jamais sur la valeur ni sur l'instant.** Un `relay-router` au +> palier 0 n'a aucun relais actif : interroger les relais *actifs* ferait osciller la réponse +> d'un cycle à l'autre, sur une installation dont rien n'a changé. La question est « ces Things +> portent-ils l'état ? », posée sur **tous** les Things déclarés de la charge. + +**Le compteur explicite reste PRIORITAIRE, et ce n'est pas d'abord une question de précision.** +Un compteur divisionnaire posé exprès mesure mieux qu'un appareil qui se déclare — c'est vrai, +et ce n'est pas la raison principale. La raison principale est qu'**un réglage qui n'a pas +d'effet est pire que pas de réglage** : si le moteur préférait l'appareil au compteur que +l'installateur a pris la peine de désigner, le champ deviendrait décoratif, et le prochain à le +lire ne saurait pas s'il sert. + +> **La priorité porte sur la SOURCE, pas sur la valeur.** `meterThingId` renseigné mais Thing +> absent, ou Thing sans l'état : on retombe sur l'appareil, et **ce qui est publié dit lequel a +> servi** (LM-1106). Ne rien publier au motif que le compteur déclaré est injoignable +> perdrait une mesure qu'on a — et le silence se lirait « charge non mesurable », ce qui serait +> faux. + +**Ce que LM-1105 ne change PAS** : rien de ce que l'**arbitre** lit. LM-1101 tient mot pour +mot — la mesure vérifie, elle ne budgète pas — et le recrédit anti-clignotement continue de lire +`LoadContext::telemetry.currentPowerW`, qui vient de l'adaptateur et n'a jamais transité par +`meterThingId`. Ce lot déplace **ce qui est publié**, pas ce qui décide. En particulier, il ne +rouvre pas la correction B ni l'artefact du banc `.75` (`AGENTS.md`, PLAN 3C). + +**LM-1106 — Ce qui est publié n'est pas un nombre : c'est un couple {valeur, source}.** +*(Second point soulevé par l'agent app, et c'est lui qui décide de la forme.)* + +Le moteur porte **déjà** trois régimes sous un seul nom, et il jette la distinction : + +| Adaptateur | Ce que vaut `telemetry().currentPowerW` | Régime | +|---|---|---| +| `evcharger` | `EvCharger::currentPower()` | **mesuré par l'appareil** | +| `etmvariableload` | l'état `currentPowerW` du Thing s'il existe ; sinon la consigne en vol ; sinon 0 | **mesuré**, ou **commandé** | +| `relay-router` | somme des relais qui portent `currentPower` ; **sinon le palier NOMINAL** | mesuré ou **nominal** — et le booléen `metered` est calculé, puis jeté | +| `sg-ready` | `estimatedPowerW[état]` | **déclaré**, jamais mesuré, délibérément (invariant 8) | + +Publier ces quatre lignes sous le même `measuredW` serait faux pour deux d'entre elles. Et +publier un `measuredW` **automatiquement rempli** détruirait au passage la seule information que +le champ portait jusqu'ici : sa présence signifiait « un compteur a été posé exprès ». + +**Donc le régime se PUBLIE, il ne se déduit pas d'une valeur.** C'est C2 de LM-1009, mot pour +mot, appliqué à la mesure — et pour la même raison qu'en LM-1009 : *un champ absent se voit ; un +champ qui s'efface se lit comme un zéro.* + +``` +o:measurement { + source : "meter" | "device" | "none", + o:powerW: double // absent si source == "none" +} +``` + +- **`meter`** — un Thing tiers déclaré par `meterThingId`. +- **`device`** — l'appareil piloté publie sa propre puissance. +- **`none`** — **rien ne mesure cette charge.** État à part entière, et positif : c'est C1 de + LM-1009 appliqué ici — une capacité manquante **se dit**. Sans lui, l'app ne peut pas + distinguer « pas mesurable » de « compteur en panne », et continue d'afficher un bloc de + mesure vide sur un `sg-ready` qui n'en aura jamais. + +> **Sous `none`, aucune valeur n'est publiée sous ce nom.** La puissance *nominale* existe et +> reste lisible — `mechanism.stageW`, `mechanism.state`, `mechanism.estimatedPowerW` la portent +> déjà, et disent d'où elle vient. La faire passer pour une mesure produirait un écart +> commande↔mesure **identiquement nul, à tous les cycles, pour toujours** : un « tout +> concorde » permanent, c'est-à-dire le pire des diagnostics. C'est le corollaire LM-104 — +> l'hypothèse de prudence devient un masquage de panne. + +**Ce que la source change au DIAGNOSTIC** (LM-1102), et c'est ce qui la rend utile plutôt que +décorative : + +| Source | Ce qu'un écart commandé↔mesuré autorise à conclure | +|---|---| +| `device` | **La charge n'obéit pas.** Commande et mesure sortent de la même boîte : l'écart parle de la charge et d'elle seule — véhicule qui refuse le courant, thermostat ouvert, plafond interne. | +| `meter` | **La charge, le compteur, ou le câblage.** Trois hypothèses, l'écart n'en tranche aucune seul. C'est le seul cas où « le compteur ne voit pas cette charge » est sur la table — le défaut ouvert du banc, compteur racine aveugle aux résistances ECS. | +| `none` | **Rien.** Aucun écart n'est calculable, et il ne faut surtout pas en fabriquer un. | + +> **L'asymétrie va plus loin que « le compteur mesure mieux », et elle donne à LM-1104 sa +> raison d'être.** Sous `device`, une **concordance** ne prouve pas grand-chose : plusieurs +> wallbox renvoient leur consigne en guise de puissance, et commandé == mesuré n'y est qu'une +> tautologie. Sous `meter`, une concordance est une **preuve**. Autrement dit : `device` +> détecte la **désobéissance**, `meter` est le seul à pouvoir détecter un **mensonge**. C'est +> pourquoi `meterThingId` doit rester **autorisé** sur une charge qui publie déjà sa puissance +> — LM-1104 ne devient pas caduc, il gagne un usage. + +**Conséquence pour l'app, et elle est de présentation, pas de modèle** : le champ « compteur +dédié » se **masque par défaut** là où `source` vaut `device`, et se propose en réglage avancé. +Il ne doit pas être *interdit* : ce serait fermer le cas ci-dessus, et casser le compteur de +tableau divisionnaire couvrant plusieurs charges (LM-1104). + --- ## §12 — Le plancher de modulation