From c5bed1f26d0ae4279dc2950ef794ae1b114a80f6 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Wed, 26 Aug 2026 10:24:39 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec):=20lot=20E=20=E2=80=94=20besoin=20?= =?UTF-8?q?=C3=A0=20deux=20niveaux,=20compteur/sonde,=20plancher=20(0.2.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois manques du modèle, spécifiés AVANT implémentation. Rien à coder tant que Patrick n'a pas relu. §10 — Besoin à deux niveaux (LM-1000+). L'éco n'est pas un champ nouveau : c'est `needs {dailyDeadline, minEnergyWhPerDay}` promu au rang de plancher. Deux passes sur la MÊME liste ordonnée, soit un ordre lexicographique sur (niveau, rang) — dérivé du rang existant, donc pas un second système de priorité ; la paire prioritéEco/prioritéConfort est rejetée explicitement. Deux règles que la conception appelle et que le brief ne demandait pas : une seule commande par charge et par cycle (sinon deux commutations dans le même cycle, du court-cycling fabriqué par l'arbitre), et un recrédit anti-clignotement attribué une seule fois. La frontière est nommée : la passe 1 n'est pas bornée par le surplus mais par une AUTORISATION DE SOUTIRAGE, objet distinct que le rule-based rendra binaire et le MILP continu. Et deux couplages sans lesquels la conception est fausse : la limite de raccordement — les planchers éco s'additionnent sur le disjoncteur, et la protection de surcharge ne déleste aujourd'hui QUE les bornes — et l'ordre de service apparent, qu'un plancher de rang 4 passant devant un confort de rang 1 rend contre-intuitif. §11 — Compteur et sonde (LM-1100+). Sélection par interface nymea, jamais par plugin. Le compteur vérifie, il ne décide pas : ne pas l'injecter au budget, il est en retard sur la commande et la charge est déjà comptée au compteur racine — décomposition, pas source. Un écart de mesure ne pose PAS available:false : ce serait retirer le service sur un doute de mesure. Et ni le compteur ni la sonde ne sont exclusifs, contrairement aux relais : LM-201 ne s'applique pas à ce qui mesure, donc validateSet() ne doit pas les inclure dans son contrôle d'unicité. §12 — Le plancher (LM-1200+). Le domaine de la consigne devient {0} ∪ [min, max], un ensemble discontinu. Déclaré pour etmvariableload seul ; dérivé pour les deux autres — même règle qu'au lot D pour stagesW. La règle d'allocation qui compte : sous le plancher, allouer 0 et TRANSMETTRE le budget, sinon une charge de rang 1 bloque tout le waterfall en gardant un budget qu'elle ne peut pas utiliser. Décision arrêtée : la priorité porte sur la borne, pas sur la voiture ; pas de priority dans ChargingInfo, qui ne saurait pas exprimer « VE1 > ECS > VE2 ». §13 — Ordre recommandé, et ce n'est PAS celui de l'énoncé : §11 d'abord (additif, risque nul, et la sonde constitue l'historique sans lequel les seuils du §10 ne se règleront jamais — le différer coûte du temps CALENDAIRE, pas du développement), puis §12 (contenu, débloque 3g puis l'unification du rang), puis §10 en dernier — et son délestage général dans le même lot, pas après. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H --- specs/spec_loadmodel.md | 313 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 311 insertions(+), 2 deletions(-) diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index c5f37fc..340dfac 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -1,12 +1,18 @@ # SPEC — Modèle de charges (domaine × mécanisme) -Version : 0.1.4 +Version : 0.2.0 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.2.0** — trois manques du modèle spécifiés, **avant** implémentation : le besoin à deux +> niveaux (§10, LM-1000+), le compteur et la sonde par charge (§11, LM-1100+), le plancher de +> modulation (§12, LM-1200+). Écrits maintenant parce qu'écrits tard ils se heurteraient à des +> structures déjà figées. **Aucun code ne suit ce document** — l'ordre d'implémentation +> recommandé est en §13, et il n'est pas celui dans lequel les trois ont été demandés. + > **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 @@ -451,7 +457,16 @@ 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. **`heating` vs `hvac` — ce que ces deux codes désignent.** Question **ouverte, et +5. **Autorisation de soutirage (LM-1005).** Quel objet exactement — un booléen d'horloge, une + fenêtre tarifaire, un coût plafond ? Le rule-based et le MILP n'en attendent pas la même + forme, et la choisir trop tôt contraindrait le second. +6. **Seuil et fenêtre du diagnostic de mesure (LM-1102).** « Trois cycles » est un point de + départ. La valeur juste dépend de l'inertie de la charge et de la latence du compteur — donc + des données que LM-1103 doit d'abord constituer. +7. **Cible de confort : en énergie ou en grandeur physique ?** Le plancher éco s'exprime en Wh + (`minEnergyWhPerDay`). Le confort s'exprime plus naturellement en **température cible**, que + le moteur ne sait pas convertir en watts sans modèle thermique. À trancher avec §11-1103. +8. **`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 @@ -469,3 +484,297 @@ distinct, pas comme une variante. - 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. + +> Les §10 à §13 qui suivent sont **postérieurs** à ces deux sections de clôture : ils ont été +> ajoutés en 0.2.0 sans renuméroter le document, pour ne pas casser les renvois existants vers +> « §8-1 ». Les points ouverts qu'ils soulèvent ont été ajoutés au §8. + +--- + +## §10 — Besoin à deux niveaux : éco et confort + +> **Statut : intention de conception.** Rien de ceci n'est implémenté. Le modèle actuel n'a +> qu'un seul niveau de besoin, et il est implicite : *tout ce que le surplus permet*. + +**LM-1000 — Une charge thermique n'a pas un besoin binaire.** Elle en a deux, de natures +différentes : + +- **Éco** — un **plancher à atteindre quoi qu'il arrive**. Un ballon qui n'a pas vu 50 °C de + la journée doit les voir, au réseau s'il le faut. C'est une **obligation d'énergie sur un + horizon**, pas une préférence d'allocation. +- **Confort** — un **bonus**, servi seulement quand le surplus abonde. Monter le même ballon + à 60 °C parce qu'il y a du soleil. Ne rien servir n'est pas un échec. + +Les deux ne se distinguent pas par leur quantité mais par ce qui arrive **quand on ne les +sert pas** : rien pour le confort, une obligation reportée pour l'éco. + +**LM-1001 — L'éco n'est pas un champ nouveau : c'est `needs` promu.** `LoadConfig` porte déjà +`needs { dailyDeadline, minEnergyWhPerDay }`. C'est **exactement** la forme d'un plancher éco : +une énergie, une échéance. Ce qui manque n'est pas la déclaration du plancher — c'est la +**cible de confort** et la **place des deux dans le waterfall**. + +> N'ajouter un `ecoNeeds` séparé serait dupliquer `needs` sous un autre nom, et il faudrait +> ensuite expliquer lequel des deux fait foi. + +**LM-1002 — Deux passes sur la MÊME liste ordonnée. Pas de second rang.** + +``` +passe 1 — ÉCO : parcourir les charges par rang croissant, servir leur plancher éco +passe 2 — CONFORT : reparcourir la MÊME liste, dans le MÊME ordre, avec le reliquat +``` + +Le rang garde un sens unique et un seul — l'ordre de service que l'installateur a posé. La +hiérarchie éco/confort le **surplombe** : elle dit *quel besoin* est servi avant, jamais +*quelle charge* passe devant. + +**Ce que cela vaut, formellement** : un ordre lexicographique sur `(niveau, rang)`, où +`niveau ∈ {éco, confort}`. C'est un ordre **total**, dérivé du rang existant — pas un second +système de priorité. + +> **Rejeté explicitement** : une paire `prioritéEco` / `prioritéConfort` par charge. Elle +> permettrait de contredire le rang, elle divergerait de lui à la première modification, et +> elle obligerait l'installateur à tenir deux classements cohérents dans sa tête. + +**LM-1003 — Deux passes, mais UNE SEULE commande par charge et par cycle.** Les passes +calculent des **cibles**, elles ne commandent pas. La consigne appliquée à une charge est la +**somme** de ce que les deux passes lui ont attribué, arrondie une seule fois au mécanisme +(palier de relais, état SG-Ready, consigne continue), puis émise une seule fois. + +C'est la règle qui empêche le défaut le plus probable de cette conception : un relais commandé +en passe 1 puis recommandé en passe 2, donc deux commutations dans le même cycle — du +court-cycling fabriqué par l'arbitre lui-même. + +**LM-1004 — Le recrédit anti-clignotement s'attribue UNE fois.** La correction B rend à chaque +charge sa consommation de début de cycle avant d'arrondir. Avec deux passes, ce crédit doit +être attribué à la **première** passe où la charge apparaît, jamais aux deux : compté deux +fois, il fabriquerait du budget qui n'existe pas. + +**LM-1005 — La frontière que ce modèle franchit, et il faut la nommer.** + +Jusqu'ici l'arbitre **distribue un surplus**. Un plancher éco non atteint au coucher du soleil +signifie **soutirer au réseau** — donc décider d'**acheter**. L'arbitre cesse d'être un +répartiteur pour devenir un acheteur, et cela ouvre trois questions qu'il n'avait jamais eu à +poser : à quel **prix**, à quelle **heure**, sous quel **signal tarifaire** (heures creuses, +Tempo). + +- **Le rule-based franchira cette frontière grossièrement**, et c'est acceptable : « plancher + non atteint à 22 h → chauffer ». Une règle d'horloge, pas une optimisation. +- **Le MILP la franchira vraiment** : c'est précisément le problème qu'il sait résoudre — + placer une énergie obligatoire dans une fenêtre, au coût minimal, sous contraintes. + +**Conséquence de conception** : la passe 1 n'est **pas** bornée par le surplus. Elle est bornée +par une **autorisation de soutirage** — un objet distinct, que le rule-based rendra binaire et +que le MILP rendra continu. La passe 2, elle, reste strictement bornée par le reliquat de +surplus. **Ne pas confondre les deux bornes** : c'est ce qui distingue un plancher d'un bonus. + +**LM-1006 — Deux couplages à traiter en même temps, sinon la conception est fausse.** + +1. **Le raccordement.** Si tous les planchers éco se servent au réseau le même matin sans + soleil, ils s'additionnent sur le disjoncteur de branchement. La passe 1 doit donc être + bornée par la **limite de raccordement** — aujourd'hui `phasePowerLimit`, dont la protection + de surcharge ne déleste **que les bornes de recharge**. Éco et délestage général sont le + même lot ; les séparer livrerait un plancher capable de faire disjoncter une installation. +2. **L'ordre de service apparent.** Un plancher éco de rang 4 passe devant un confort de + rang 1. C'est voulu, et c'est **contre-intuitif** pour l'installateur qui vient de poser son + ordre. L'app doit le montrer — deux passes visibles, pas un classement unique qui aurait + l'air désobéi. Sans cela, le premier retour terrain sera « l'ordre n'est pas respecté ». + +**LM-1007 — Compatibilité descendante, et elle est gratuite.** Une charge qui ne déclare +**aucun** besoin éco ne participe qu'à la passe 2 — soit exactement le comportement +d'aujourd'hui. Le modèle à deux niveaux **dégrade proprement** vers le modèle actuel, ce qui +permet de l'implémenter sans migration de configuration. + +--- + +## §11 — Compteur et sonde par charge + +> **Statut : intention de conception.** Deux champs optionnels, de **même forme**, et dont +> l'arbitre ne se sert **pas pour décider** dans ce lot. + +**LM-1100 — Deux champs, une même règle.** + +``` +LoadConfig { + … + o:meterThingId : ThingId — mesure de CETTE charge + o:sensorThingId : ThingId — grandeur physique du service rendu (température) +} +``` + +**Sélection par interface nymea, jamais par plugin.** Le compteur se choisit parmi les Things +portant `energymeter` / `smartmeter` ; la sonde parmi celles portant l'interface de température. +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. + +**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. + +**Ne PAS injecter la mesure dans le budget.** Deux raisons, et chacune suffit : + +1. **Elle est en retard sur la commande** — scrutation Modbus, moyennage interne du compteur. + L'injecter brute réintroduirait exactement l'oscillation que le recrédit de la correction B + a supprimée : commander, mesurer trop bas, recommander, dépasser, corriger. +2. **La charge est déjà comptée dans le compteur racine.** Le compteur de charge est une + **décomposition** de ce que le racine mesure, pas une source à additionner. L'ajouter + compterait la même énergie deux fois. + +**LM-1102 — Un écart de mesure n'est pas un défaut d'écriture.** `WRITE_FAILED` dit « l'échelle +de commande est épuisée » ; un écart de mesure dit « la commande est partie, l'effet n'est pas +là ». Deux causes, deux gestes chez le client : l'une envoie au bus, l'autre au disjoncteur +divisionnaire ou à la résistance. + +**L'écart NE DOIT PAS poser `available: false`.** Sortir une charge de l'arbitrage sur un doute +de **mesure** — alors que la commande, elle, est peut-être partie — coûterait le service sur un +soupçon. Il se publie comme **diagnostic**, à côté de `faultCode` et sans le remplacer. + +> Le seuil et le nombre de cycles sont des **paramètres**, pas des constantes : un compteur +> lent et une charge à forte inertie produiraient sinon un diagnostic permanent. Trois cycles +> est un point de départ, pas une valeur figée. + +**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 : + +1. **Diagnostic de mise en service** — l'installateur voit la température monter pendant qu'il + commande, ou ne pas monter. +2. **Constituer l'historique** — température, puissance, heure. **Sans lui, les seuils éco et + confort du §10 ne pourront jamais être réglés autrement qu'au doigt mouillé.** Un ballon ne + révèle sa constante de temps qu'observé, et une PAC ne révèle sa courbe qu'à travers une + saison. C'est la raison pour laquelle ce champ mérite d'exister **avant** d'avoir un + consommateur : le jour où le §10 s'implémente, il faut déjà des données. + +**LM-1104 — Ni le compteur ni la sonde ne sont exclusifs, contrairement aux relais.** LM-201 +(« un canal de commande, une charge ») ne s'applique pas ici : ces Things ne **commandent** +rien, ils mesurent. Deux charges peuvent légitimement partager une sonde de pièce, et un +compteur de tableau divisionnaire peut couvrir plusieurs charges. + +**Conséquence directe** : `validateSet()` ne doit **pas** inclure `meterThingId` ni +`sensorThingId` dans son contrôle d'unicité des Things. Les y ajouter par symétrie avec +`relays[]` refuserait des configurations correctes. + +--- + +## §12 — Le plancher de modulation + +> **Statut : intention de conception.** C'est le **préalable à l'étape 3g** — la +> transplantation du VE dans le waterfall. + +**LM-1200 — Le modèle n'exprime aujourd'hui qu'une enveloppe proportionnelle.** Une charge +modulable est décrite par `maxPowerW`, et l'arbitre suppose qu'elle accepte n'importe quoi +entre 0 et ce plafond. **C'est faux pour la plupart du matériel réel** : + +- une borne de recharge **ne charge pas sous 6 A** — sous ce seuil elle ne démarre pas, ou + démarre et s'arrête ; +- les **paliers SG-Ready sont un plancher déguisé** : l'état 3 consomme ce qu'il consomme, il + n'existe pas d'état « 3 à moitié » ; +- un routeur de relais a le même plancher implicite : son plus petit palier non nul. + +**LM-1201 — Spécifier : rien, ou au moins X.** Une charge modulable porte un minimum non nul : + +``` +adapter = "etmvariableload" → { mode: "dynamic", maxPowerW, o:minPowerW } +``` + +Le domaine de la consigne cesse d'être `[0, maxPowerW]` pour devenir +**`{0} ∪ [minPowerW, maxPowerW]`** — un ensemble discontinu. C'est la seule façon de dire ce +que le matériel fait vraiment. + +**LM-1202 — Le plancher existe pour les trois mécanismes ; il n'est déclaré que pour un.** + +| Mécanisme | Plancher | Origine | +|---|---|---| +| `etmvariableload` | `minPowerW` | **déclaré** — rien ne permet de le deviner | +| `relay-router` | plus petit palier non nul | **dérivé** de `relays[]` | +| `sg-ready` | `estimatedPowerW` de l'état 3 | **dérivé** de la table d'états | + +Ne pas déclarer ce qui se dérive : c'est la règle que le lot D vient d'appliquer aux paliers +(`stagesW`), et pour la même raison — deux sources d'une même vérité divergent. + +**LM-1203 — Un budget inférieur au plancher ne doit pas être retenu en otage.** Règle +d'allocation : + +``` +si budget_disponible < plancher(charge) → allouer 0, et TRANSMETTRE le budget à la suivante +``` + +Sans cette règle, une charge de rang 1 au plancher élevé bloquerait tout le waterfall en aval +en gardant un budget qu'elle ne peut pas utiliser. + +**LM-1204 — Pourquoi c'est le préalable à 3g, et pas un raffinement.** + +Deux bornes, 2 kW de surplus, plancher de 1,38 kW chacune (6 A monophasé) : + +- **répartition égale** : 1 kW à chacune → **les deux sont sous leur plancher, rien ne charge, + et les 2 kW partent au réseau** ; +- **waterfall** : 1,38 kW à la première, le reliquat de 0,62 kW à la seconde qui n'atteint pas + son plancher et reçoit donc 0 → **une voiture charge**. + +Le proxy actuel, qui décide du VE **en amont** du waterfall, ne peut pas produire le second +comportement : il n'a pas de notion de reliquat à passer. **C'est le plancher qui rend la +transplantation nécessaire, pas l'inverse.** + +**LM-1205 — La machinerie d'échéance existe déjà.** `needs { dailyDeadline, minEnergyWhPerDay }` +est ce qui avait motivé le proxy — la crainte que le waterfall ne sache pas exprimer « cette +voiture doit être chargée à 7 h ». Il sait : c'est le même champ que le plancher éco du §10 +(LM-1001). Une fois le VE dans le waterfall, il hérite du modèle de besoin commun. + +**LM-1206 — La priorité porte sur la BORNE, pas sur la voiture. Décision arrêtée.** Il n'existe +pas de communication véhicule aujourd'hui : rien ne permet de savoir *quelle* voiture est +branchée, ni de lui attribuer un rang. Le rang appartient donc au **point de charge**, comme il +appartient à un chauffe-eau. + +`assignedCarId` existe déjà dans `ChargingInfo` : le jour où la communication véhicule arrive, +la migration du rang de la borne vers le véhicule reste possible **sans casser l'acquis**. + +**LM-1207 — Ne PAS ajouter de `priority` à `ChargingInfo`.** Ce serait un **second système de +priorité**, et il serait immédiatement insuffisant : il ne saurait pas exprimer +**« VE1 > ECS > VE2 »**, puisqu'il ne classerait que les bornes entre elles. Deux classements +séparés qui ne peuvent pas s'interclasser ne sont pas deux fonctionnalités, c'est un défaut. + +Le rang du VE doit vivre **là où vivent tous les rangs** : dans `LoadConfig.priority`. C'est +une conséquence directe de 3g, et une raison de plus de ne pas le différer. + +--- + +## §13 — Ordre d'implémentation recommandé + +**Ce n'est pas l'ordre dans lequel les trois manques ont été énoncés**, et la raison tient à ce +que chacun coûte et rapporte. + +**1. §11 — compteur et sonde.** *Le moins cher, et il conditionne le reste.* + +Purement **additif** : deux champs optionnels, aucune décision d'arbitrage modifiée, aucune +migration de configuration. Le risque de régression est proche de zéro. + +Mais surtout : **la sonde constitue l'historique sans lequel les seuils du §10 ne pourront pas +être réglés.** Un ballon révèle sa constante de temps en quelques jours, une PAC en une saison. +Chaque semaine où ce champ n'existe pas est une semaine de données qu'on n'aura jamais. +Le différer ne coûte pas du temps de développement — il coûte du **temps calendaire**. + +**2. §12 — le plancher.** *Contenu, et il débloque 3g.* + +Le plancher est **dérivable** pour deux mécanismes sur trois : seul `etmvariableload` demande un +champ. Le vrai travail est la règle d'allocation LM-1203 (« sous le plancher → 0, et on +transmet »), qui est locale au waterfall et se teste sans matériel. + +Il débloque 3g, qui débloque à son tour l'unification du rang (LM-1207). Trois choses attendent +derrière une seule. + +**3. §10 — éco / confort.** *Le plus gros, et le seul qui franchisse une frontière.* + +À faire en dernier, pour trois raisons : + +- il **change le waterfall lui-même**, alors que les deux précédents l'entourent ; +- il fait de l'arbitre un **acheteur**, ce qui ouvre le prix, l'heure et le signal tarifaire ; +- **il est indissociable du délestage général** (LM-1006-1), qui n'existe pas encore : la + protection de surcharge ne déleste que les bornes. Livrer les planchers éco sans lui, c'est + livrer un mécanisme capable de faire disjoncter une installation un matin sans soleil. + +> **Recommandation ferme sur ce dernier point** : traiter l'extension du délestage au waterfall +> **dans le même lot** que les planchers éco, ou avant. Pas après. + +**Ce qui peut se faire en parallèle, sans rien attendre** : la publication en télémétrie de la +sonde et du diagnostic de mesure (§11), puisqu'elle n'a aucun effet sur les décisions. + +---