diff --git a/AGENTS.md b/AGENTS.md index f03586d..76ea125 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -209,6 +209,22 @@ la somme des charges mesurées** (3 250 W contre 3 980 W pour la seule V2C, plus Même famille que l'aveuglement documenté aux résistances ECS. Ce n'est pas un problème d'arbitrage — câblage ou vue SunSpec — mais un budget juste suppose un compteur qui voit tout. +**DÉCIDÉ, PAS ENCORE ÉCRIT (2026-08-28)** — **la mesure par charge se lit sur l'appareil +piloté**, `specs/spec_loadmodel.md` **LM-1105/LM-1106**, sur un constat d'usage de l'agent app. +`meterThingId` était la SEULE source : il fallait désigner une borne comme son propre compteur +pour qu'elle publie ce qu'elle mesure déjà. Il reste **prioritaire** — un réglage sans effet est +pire que pas de réglage — mais cesse d'être requis. + +Ce que la vérification a trouvé, et qui décide de la forme : `telemetry().currentPowerW` porte +**déjà trois régimes** sous un seul nom — mesuré (`evcharger`), mesuré OU palier nominal +(`relay-router`, dont le booléen `metered` est calculé puis jeté), déclaré (`sg-ready`, +délibérément, invariant 8). D'où `measurement {source, o:powerW}` en remplacement de +`measuredW`, avec `source: "none"` comme état positif. **Contrat déplacé** : `INTERFACE.md`. + +> Ce lot 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`. Ne pas y voir une réouverture de la correction B. + **PROCHAINE ACTION** : **le rang par défaut des bornes auto-provisionnées**, qui retombe à égalité avec le rang 1 déjà occupé au lieu de mettre la borne « en tête » (`docs/RELEVE_3g2.md` §3.2). Trois issues possibles, aucune tranchée — autoriser le rang 0, diff --git a/INTERFACE.md b/INTERFACE.md index 7ca0207..3608d85 100644 --- a/INTERFACE.md +++ b/INTERFACE.md @@ -408,8 +408,16 @@ charge — jamais ce qu'on lui **commande**. plugin rendrait le modèle faux au prochain qui rend le même service. - **Aucun contrôle d'existence à l'écriture.** Un `ThingId` encore inconnu est accepté : refuser imposerait un ordre aux opérations — déclarer le compteur avant la charge — que rien ne - justifie. Un id qui ne résout pas produit une **absence de mesure**, pas une configuration + justifie. Un id qui ne résout pas fait **retomber sur la mesure de l'appareil** si elle + existe (`measurement.source: "device"`), sinon produit `"none"` — jamais une configuration invalide. +- **`meterThingId` n'est plus REQUIS pour obtenir une mesure** *(LM-1105, 2026-08-28)*. Quand le + Thing piloté porte lui-même l'état de puissance — borne, `etmvariableload`, relais mesurant — + le moteur le lit sans configuration, et publie `measurement.source: "device"`. Le champ reste + **autorisé partout** et **prioritaire** quand il est renseigné : un compteur posé exprès est + le seul à pouvoir contredire un appareil qui se déclare. Ce qui change est qu'il cesse d'être + une corvée là où le moteur peut déduire — un client avisé le **masque par défaut** sur les + charges dont `source` vaut déjà `device`, sans l'interdire. - **Ni l'un ni l'autre n'est exclusif**, contrairement à `relays[]`. Ils mesurent, ils ne commandent pas : `validateSet()` les ignore, et deux charges peuvent partager une sonde de pièce ou un compteur de tableau divisionnaire. @@ -651,11 +659,44 @@ le **libellé de la charge n'y figure jamais** (le client le résout via `GetLoa > La distinction est le fruit d'ECS-309 et ECS-309-b (corrections de terrain) — un motif faux > envoie diagnostiquer le mauvais problème, avec l'autorité d'une explication. -**`measuredW`** *(o:)* — puissance lue sur le compteur dédié, quand la charge en déclare un. +**`measurement`** *(o:)* — ce qui est **observé** de la charge, avec sa **source**. +*(Remplace `measuredW` — voir la note de migration ci-dessous. Décidé le 2026-08-28, +`specs/spec_loadmodel.md` **LM-1105/LM-1106**.)* + +```jsonc +"measurement": { + "source": "meter" | "device" | "none", + "powerW": 3980.0 // o: — absent si source == "none" +} +``` + +| `source` | D'où vient `powerW` | +|---|---| +| `meter` | Un Thing tiers, désigné par `meterThingId`. **Prioritaire** quand il résout et porte l'état | +| `device` | L'appareil piloté publie lui-même sa puissance — borne, `etmvariableload`, relais mesurant | +| `none` | **Rien ne mesure cette charge.** `powerW` est alors ABSENT, jamais nul | + Publiée **à côté** de `allocatedW`, jamais fondue dedans : `allocatedW` est ce que le waterfall -a **alloué**, `measuredW` ce qu'un compteur a **vu**. Les confondre rendrait la somme des -allocations irréconciliable avec `budget.allocatedW`. Absente si la charge ne déclare pas de -compteur, ou si le Thing ne porte pas l'état. +a **alloué**, `measurement.powerW` ce qui a été **vu**. Les confondre rendrait la somme des +allocations irréconciliable avec `budget.allocatedW`. + +> **Pourquoi la source est dans la charge utile et pas déduite.** Un écart commandé↔mesuré ne +> conclut pas la même chose selon elle : sous `device` il dit « la charge n'obéit pas » (commande +> et mesure sortent de la même boîte) ; sous `meter` il ouvre trois hypothèses — la charge, le +> compteur, ou le câblage ; sous `none` il n'est **pas calculable**, et le fabriquer depuis la +> valeur nominale produirait un écart identiquement nul à tous les cycles, c'est-à-dire un +> « tout concorde » permanent. +> +> `none` est un état **positif**, pas une absence : il dit « cette charge n'est pas mesurable » +> — un `sg-ready` sur contacts secs ne le sera jamais — et permet à un client de **masquer** le +> bloc de mesure au lieu d'afficher un vide. Sans lui, « pas mesurable » et « compteur en +> panne » se ressemblent. + +> **Migration depuis `measuredW`.** L'ancienne clé disparaît ; elle ne pouvait pas exprimer +> trois états, et la doubler d'un second champ aurait obligé le client à joindre deux clés pour +> lire une seule chose. `measuredW: X` devient `measurement: {source: …, powerW: X}` ; son +> **absence** devient `measurement.source: "none"` — ce qui est précisément l'information qui +> manquait. Un seul écran est concerné. **`temperatureC`** *(o:)* — température lue sur la sonde déclarée. **Lecture seule, aucun effet sur les décisions.** Son usage immédiat est le diagnostic de mise en service ; son usage durable diff --git a/docs/BRIEF_depuis_plugin.md b/docs/BRIEF_depuis_plugin.md index c8b4584..06b1c02 100644 --- a/docs/BRIEF_depuis_plugin.md +++ b/docs/BRIEF_depuis_plugin.md @@ -5,7 +5,8 @@ **Contexte** : `docs/RELEVE_3g2.md` (ce dépôt) porte les traces machine, cycle par cycle. **Contrat** : `INTERFACE.md` fait autorité — ce brief ne fait qu'en signaler les mouvements. -> Six points. Le sixième répond à vos quatre constats du 28 août. +> Sept points. Le sixième répond à vos quatre constats du 28 août, le septième à votre +> constat d'usage sur le compteur par charge — et celui-là **bouge un champ du contrat**. > Le deuxième **retire une mise en garde** que vous portiez — du code conditionnel > peut disparaître. Le cinquième répond à **votre** question sur `targetPercentage`, et c'est > celui qui change un écran : l'avancement s'affichera en énergie livrée, pas en pourcentage. @@ -267,6 +268,73 @@ Et `targetPercentage = 80` observé sur la V2C **confirme sur machine** le défa --- +## 7. Le compteur par charge : vous avez raison, et le modèle bouge + +*(Votre constat d'usage du 2026-08-28. Tranché : `spec_loadmodel.md` **LM-1105** et +**LM-1106**. Contrat : `INTERFACE.md`. **Pas encore implémenté** — rien à consommer aujourd'hui.)* + +### Le constat est retenu tel quel + +Désigner une borne comme **son propre compteur** pour qu'elle affiche ce qu'elle mesure déjà +n'est devinable par personne. Une note d'écran qui explique un réglage est le signe que le +défaut est dans le **modèle**, pas dans l'app — et c'est de la configuration demandée à +l'installateur pour compenser ce que le moteur peut déduire. **Le moteur lira l'appareil qu'il +pilote** dès qu'il porte lui-même l'état de puissance. + +Une précision de rédaction : 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. + +### Vos deux points, tranchés + +**1. Le compteur explicite reste prioritaire — oui, et c'est écrit.** Mais la raison principale +n'est pas la précision : c'est qu'**un réglage sans 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. La priorité porte sur la **source**, pas sur la valeur : compteur +déclaré mais Thing absent → on retombe sur l'appareil, et la charge utile **dit lequel a servi**. + +**2. Votre second point est le plus important, et il décide de la FORME.** En le vérifiant, on a +trouvé que le moteur porte **déjà** trois régimes sous un seul nom — et qu'il jette la +distinction : une borne rend une mesure ; un `relay-router` rend une mesure **ou le palier +nominal** selon que ses relais mesurent (le booléen est calculé, puis perdu) ; un `sg-ready` +rend **toujours** une puissance déclarée, jamais mesurée. + +Donc le régime **se publie**, il ne se déduit pas d'une valeur — c'est votre C2 de LM-1009, mot +pour mot. `measuredW` disparaît au profit de : + +```jsonc +"measurement": { "source": "meter" | "device" | "none", "powerW": 3980.0 } +``` + +`powerW` est **absent** sous `none`. Et `none` est un état **positif** : « cette charge n'est pas +mesurable ». C'est lui qui vous permet de **masquer** le bloc de mesure au lieu d'afficher un +vide sur un `sg-ready` qui ne le sera jamais — et de distinguer « pas mesurable » de « compteur +en panne », que rien ne séparait. + +### Ce que la source change pour vos écrans de diagnostic + +| `source` | Ce qu'un écart commandé↔mesuré autorise à dire | +|---|---| +| `device` | **La charge n'obéit pas** — véhicule qui refuse, thermostat ouvert, plafond interne | +| `meter` | **La charge, le compteur, ou le câblage** — trois hypothèses, l'écart n'en tranche aucune | +| `none` | **Rien.** Pas d'écart affichable, et surtout pas un « 0 W d'écart » | + +> Une asymétrie qui va plus loin que « le compteur mesure mieux », et qui vaut d'être dite à +> l'écran : sous `device`, une **concordance** ne prouve rien — plusieurs wallbox renvoient leur +> consigne en guise de puissance. Sous `meter`, elle est une **preuve**. `device` détecte la +> désobéissance ; `meter` est le seul à pouvoir détecter un **mensonge**. C'est pourquoi +> `meterThingId` reste **autorisé** sur une borne qui publie déjà : à masquer par défaut, pas à +> interdire. + +### Votre coût, et il est d'un écran + +`measuredW: X` → `measurement: {source, powerW: X}` ; son **absence** → `source: "none"`. +La clé n'est pas doublée pendant une transition, délibérément : deux noms pour une chose +divergent (c'est la règle qui a fait naître `renderFr`). Prévenez-nous si le calendrier de +l'app impose l'inverse. + +--- + ## Ce qui n'a PAS changé, et qu'il ne faut pas déduire - **L'échéance de départ et le tarif dynamique restent au proxy**, financés au réseau 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