146 Commits

Author SHA1 Message Date
Patrick Schurig
0c5f0ee30d docs(ev): Min+PV sert bien le plancher — prouvé par deux comptages, et boundBy ne prouve rien
+etm58 posé (au-delà de +etm57 annoncé). Trois vérifications.

1. ANCRAGE DES INSTANTS : livré et observable. Les trois champs ISO de la trame
   portent le marqueur Z, `command.since` compris —
   `{divergent, expectedW, observedW, since: "2026-09-07T14:37:00Z"}`.

2. MIN+PV : livré. La voiture a chargé toute la journée sans jamais quitter
   ChargingModeEcoWithMinCurrent, et deux comptages sans source commune
   concordent à 4 % :
     surplus MAX du jour        536 W — 0 quart d'heure sur 71 au-dessus de 4140
     fenêtre 08:45 -> 15:15     16,2 kWh dont 14,2 au-dessus de la base (539 W)
     sessionEnergy de la V2C    1,06 -> 14,72 kWh, soit +13,7 kWh
   Le surplus n'ayant jamais dépassé 536 W, tout a été acheté au réseau : c'est
   le plancher demandé.

3. `boundBy` : ABSENT, et cette absence ne prouve rien — le seul ECO_FLOOR_GRID
   de la trame est celui du chauffe-eau, dont le plancher vient d'une obligation
   quotidienne et non d'un minimum matériel ; et la borne est sortie de loads[]
   (pluggedIn: false). Un `grep -c boundBy` valant 0 aurait fait conclure
   « non livré » à l'instant où la mesure prouve le contraire. À revoir voiture
   branchée.

Et une affirmation de CLAUDE.md devenue FAUSSE, corrigée : le plafond de
soutirage du §10 existe désormais —
  "draw": { "authorisedW": 6000.4, "committedW": 103.2, "remainingW": 5897.2,
            "binding": "connection", "bindingPhase": "A", "perPhaseBound": true }
`binding` nomme ce qui contraint et `bindingPhase` la phase qui mord : le
plafond se dit donc avec sa source, la condition qu'on avait posée pour
l'afficher. Aucun écran ne le lit encore, et rien n'est câblé — l'écran du §10
n'est pas validé.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-07 16:49:34 +02:00
Patrick Schurig
a44c84a037 docs(ev): boundBy est le marqueur de présence du plancher — avec sa mesure « avant »
Le moteur donne le bon signal : `boundBy`, dans `ECO_FLOOR_GRID.params`,
n'apparaît que sur un moteur qui sert effectivement le minimum (+etm57). C'est
donc lui qui répond à « le plancher est-il servi ici ? », et pas le mode ni une
version annoncée.

Absent ne veut PAS dire « pas de plancher » : il veut dire « moteur antérieur à
+etm57 ». Même règle que levels[] vide ou counts absent — l'absence date la box,
elle ne décrit pas le cycle.

Mesure « avant » prise aujourd'hui, sans quoi un boundBy toujours absent après
livraison serait indiscernable d'un champ jamais ajouté :
  powersync-energy-plugin-nymea 1.15.2+etm56 posé
  boundBy : 0 occurrence dans toute la trame
  ECO_FLOOR_GRID.params : atMaximum, gridW, regime, remainingS, remainingWh,
                          targetWh — les six clés connues, pas une de plus

Le bouton Min+PV reste tel quel (Patrick). Aucun code conditionnel écrit : le
champ n'est pas observable, et « sonder avant de câbler » vaut aussi pour un
marqueur de présence.

+etm57 portera aussi le correctif d'ancrage des instants — à sa pose, resonder
les champs ISO pour vérifier le marqueur Z partout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-07 11:25:42 +02:00
Patrick Schurig
f2bd07d538 docs(ev): le « Min » de Min+PV ne sert aucun plancher — mesuré, et le plancher est dérivable
Sémantique voulue (Patrick, 2026-09-07) : Min+PV charge toujours au minimum de
la borne, acheté au réseau s'il le faut, et ajoute le surplus par-dessus. 400 W
d'injection sur un plancher de 4 140 W donnent 4 540 W.

Mesuré sur .75, voiture branchée et borne joignable : la borne ne reçoit qu'un
niveau `comfort`, aucun niveau `eco`, décision `SURPLUS_INSUFFICIENT`,
`chargingEnabled: false`. ChargingModeEcoWithMinCurrent se comporte exactement
comme ChargingModeEco.

Le mécanisme existe déjà — `ECO_FLOOR_GRID`, ce que le chauffe-eau fait dans la
même trame : plancher servi, acheté au réseau, compté dans draw.committedW,
surplus distribué au-dessus par comfort. Il n'est pas câblé sur la borne.

Et le blocage noté depuis longtemps (« minCurrent n'existe nulle part ») n'en
est pas un : la valeur est sur la box, dans la classe du Thing —
maxChargingCurrent.minValue = 6 A, phaseCount = 3, soit les 4 140 W attendus.
Aucun champ de contrat à ajouter, et rien à saisir côté app : une limite
matérielle se lit sur le matériel.

Pourquoi ça n'a pas été vu : sur ce banc le surplus n'atteint jamais le
plancher — 0 quart d'heure sur 97 le 2026-09-06, meilleur 2 214 W — donc les
deux modes rendent le même rien et ne se distinguent par aucune observation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-07 10:24:12 +02:00
Patrick Schurig
3710401ad6 docs(captures): l'heure locale à l'écran — « mesuré depuis 06:33 »
APK reconstruit et posé. La box publie `measuredSince: 2026-09-07T04:33:28Z` ;
l'écran affiche 06:33, l'attendu dérivé par `.toLocal()`. L'APK précédent, sur
la même box et le même téléphone tous deux en Europe/Paris, affichait 04:33 —
la preuve que l'écart ne devait rien au fuseau de la box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-07 08:31:16 +02:00
Patrick Schurig
a430fb00c8 fix(instants): un seul lecteur d'horodatage de box — et ma cause d'hier était FAUSSE
Le contrat porte désormais la règle : tout horodatage publié est en UTC avec son
marqueur, ou en epoch. Trois champs, deux formats, rien ne l'exigeait.

`instantDeLaBox` (lib/models/box_instant.dart) devient l'unique lecteur pour ce
que la box publie — epoch ou ISO, marqué ou nu — et requalifie la forme nue en
UTC avant de rendre l'heure du lecteur. Politique valable pour la BOX seule :
`Installation.lastSeen` est écrite en local sans marqueur et relue pareil, donc
la lire en UTC la décalerait. Deux héritages opposés sous une même forme, d'où
l'absence délibérée d'un « parser une date » unique.

Le `.toLocal()` posé hier dans telemetry_text est retiré : le modèle est seul
propriétaire de la conversion. Le rejouer serait sans effet mais laisserait
croire qu'elle peut manquer ailleurs.

CORRECTION. J'avais écrit que le défaut avait survécu « parce que box et
téléphone partageaient l'heure ». C'est faux, et la fausseté est utile : l'écart
est entre UTC et l'APPAREIL, donc visible sur tout téléphone hors UTC+0 quel que
soit le fuseau de la box. Mesuré ce matin avec les deux en Europe/Paris — la box
publie 04:33:28Z, l'APK d'hier affiche « 04:33 », il fallait lire 06:33. La
bascule en Europe/London n'a pas donné la visibilité, elle a donné le MOTIF de
regarder. La version fausse fait conclure « les fuseaux coïncident, donc c'est
réglé » et laisse le défaut en place.

`GetVariableLoadState` n'existe pas sur .75 (absente d'Introspect) et l'app ne
lit aucun `since` : rien à corriger de ce côté. Le champ existe en revanche dans
GetLoadTelemetry — `loads[].command {divergent, expectedW, observedW, since}` —
et il porte bien le Z, vérifié à la sonde. Aucun écran ne le consomme encore.

Contrôle négatif de la ligne d'horloge : même binaire qu'hier, box repassée en
Europe/Paris, la ligne a disparu sous le champ d'échéance.
docs/captures/2026-09-07_fuseau-box-paris-ligne-absente.png

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-07 07:34:24 +02:00
Patrick Schurig
92495c0e87 fix(dashboard): « sans production » n'est pas « pas mesurable »
Signalé sur appareil de nuit : tuile Autoconsommation « — · pas mesurable »,
sur une installation dont les quatre compteurs marchent.

D'où vient le nombre : la tuile lit `DailyBalance`, pas `GetEnergyRatios`.
`selfConsumptionRate` y rend `null` dès que `productionKwh <= 0` — production
nulle depuis minuit, donc 0/0.

Le « — » est JUSTE et ne bouge pas. Un « 0 % » affirmerait qu'aucune production
n'a été autoconsommée : une phrase sur une production qui n'existe pas, soit un
zéro forgé de plus. Ce qui était faux, c'est le motif écrit dessous — il
accusait la mesure là où il n'y avait simplement rien à mesurer.

Les deux causes d'un taux absent sont désormais séparées :
- pas de bilan (échantillons, cumuls, compteur en recul) → « pas mesurable » ;
- dénominateur nul → « sans production » / « sans consommation ».

C'est la distinction habituelle du dépôt prise par l'autre bout : d'ordinaire on
empêche une absence de se dire zéro ; ici on l'empêche d'accuser la mesure.

Mesuré sur .75 au même instant : production 0 W, `selfConsumptionRate` absent de
GetEnergyRatios sur trois relevés, `autonomyRate: 0` présent — la box fait la
même distinction, sur la même arithmétique.

Le premier jet du test asserait `find.text('0 %'), findsNothing` : il mordait
sur le 0 % de l'Autonomie, qui est une mesure. Corrigé en le voyant échouer, et
la raison est dans le test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-07 06:42:17 +02:00
Patrick Schurig
b843d8936a fix(fuseau): un INSTANT se rend chez le lecteur — « mesuré depuis » affichait l'heure UTC
Trouvé sur appareil pendant la bascule volontaire de .75 en Europe/London, en
capturant la ligne d'horloge de l'échéance : la box publie
`measuredSince: "2026-09-06T14:38:00Z"` et l'écran affichait « mesuré depuis
14:38 » — ni l'heure de la box (15:38 BST), ni celle du téléphone (16:38 CEST).
L'heure de personne.

`DateTime.tryParse` d'une chaîne en `Z` rend un DateTime `isUtc: true` ; lire
`.hour` sans `toLocal()` donne l'heure UTC. Le défaut est identiquement
invisible tant que box et appareil partagent l'offset : il a fallu DEUX fuseaux
pour qu'il se voie, et c'est la couture ouverte pour la capture qui l'a montré.

Deux natures d'heure, désormais séparées dans CLAUDE.md :
- un RÉGLAGE (`dailyDeadline`) est lu par la box → on nomme son horloge ;
- un INSTANT (`measuredSince`) est absolu → il se rend chez le lecteur, sans
  mention.

Les deux tests qui gardaient « 03:42 » en dur gelaient l'heure UTC et validaient
le défaut : leur attendu se dérive maintenant de `.toLocal()`. Le contrôle
négatif ne mord que hors UTC, et le test le dit au lieu de laisser lire un vert
comme une preuve.

Capture de la ligne d'horloge : docs/captures/2026-09-06_fuseau-box-london.png

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 19:57:38 +02:00
Patrick Schurig
9424598d58 docs(sondes): l'entrée synthétique ne reproduit pas un appui-glissé
Établi le 2026-09-06 : deux tentatives de réordonnancement par `input swipe` puis
`input motionevent`, aucun mouvement — et la conclusion « le glisser-déposer est
cassé » aurait été fausse. Le même geste au doigt, le matin même, avait produit
une écriture qui a tenu.

`input swipe` interpole une trajectoire linéaire ; un appui-glissé réel comporte
une phase de maintien que Flutter attend avant d'armer le glissement.

Ce qui rend le piège coûteux est que l'échec RESSEMBLE à un défaut : la poignée
capte le geste — la liste ne défile pas, ce qu'elle ferait s'il passait à travers
— et rien ne bouge. Un réordonnancement se vérifie donc au doigt, ou pas du tout ;
ce qu'`adb` peut vérifier, c'est le résultat relu dans `GetLoadConfig`.

Même famille que le `finally` qui défait l'action au lieu de restaurer l'état :
l'outil répond, et sa réponse ne parle pas de ce qu'on croit mesurer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 15:42:00 +02:00
Patrick Schurig
cb114b62bd fix(tableau de bord): les DEUX providers, pas un seul
Troisième tentative sur le même symptôme, et la bonne : le détail de consommation
croise la TÉLÉMÉTRIE (la mesure) et la CONFIGURATION (le domaine), et ni l'un ni
l'autre n'était attaché ailleurs que sur l'écran des rôles. Sur le tableau de
bord, `telemetry` était null ET `entries` vide.

Les deux correctifs précédents ne traitaient que la configuration — la moitié que
le premier diagnostic avait désignée. C'est la recherche auto-confirmante en
version « je n'ai regardé qu'un provider » : j'ai vérifié la partie que je venais
d'écrire, jamais l'autre, et l'écran a redit trois fois la même chose.

La donnée était pourtant sans ambiguïté à chaque étape : `chauffe-eau` → `ecs`,
`pac-terrain` → `heating`, et `{powerW: 0, source: "meter"}` sur les deux. Rien
ne manquait côté box ; tout manquait côté app, à l'endroit où on va chercher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 14:58:30 +02:00
Patrick Schurig
b3da81d277 fix(tableau de bord): charger la configuration quand la box répond, pas au montage
Le correctif précédent — charger au montage — n'a rien changé, vérifié à l'écran :
PAC et « Eau chaude » affichaient toujours « non publié » alors que la box publie
`{powerW: 0, source: "meter"}` pour les deux.

La raison est le moment : l'écran se construit avant que la connexion soit
établie, l'appel échoue en silence, et RIEN ne réessaie. Le tick existait, mais à
cinq minutes.

Deux accroches donc, et la première suffit dans le cas normal : un chargement dès
que la box devient joignable (une seule fois, gardé par un drapeau), et un
rattrapage sur le tick si la configuration est encore vide.

C'est la troisième fois aujourd'hui que le même motif se présente — une absence
côté APP présentée comme une absence côté BOX — et la deuxième fois qu'un
correctif à ce motif échoue parce qu'il était posé au mauvais instant. Le
symptôme se corrige où la donnée manque ; la cause vit dans le moment où on va la
chercher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 14:42:14 +02:00
Patrick Schurig
9edd9cc51f docs: deux contrôles qui ne mordent pas, et la question qui les démasque
La recherche auto-confirmante : après avoir corrigé le signe du solaire, la
vérification cherchait `pvPower.abs` — donc la ligne qu'on venait d'écrire, ce
qui a caché la seconde occurrence, non corrigée. On cherche la GRANDEUR, jamais
le correctif.

Le contrôle vrai par construction : `deliveredWh + remainingWh == targetWh` ne
peut pas échouer sous `unmeasurable`, et l'afficher comme « identité OK » ferait
lire un feu vert sur le régime qu'elle ne contrôle pas.

La parade est commune : qu'est-ce que ce contrôle aurait attrapé s'il avait
échoué ? Si la réponse est « rien », il décore — et il coûte plus qu'aucun
contrôle, parce qu'il occupe la place de celui qui aurait mordu.

Avec le corollaire rencontré deux fois le même jour : une absence côté APP
présentée comme une absence côté BOX. Avant d'écrire qu'une chose n'est pas
publiée, vérifier qu'on est allé la chercher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 14:21:21 +02:00
Patrick Schurig
cd202840f1 fix(flux): la production restait négative dans le DÉTAIL, et le tableau de bord ignorait la config
Deux défauts trouvés en vérifiant la correction précédente à l'écran.

1. « -1.3 kW · Solaire » dans « Production — détail ». La correction du
   2026-09-05 n'avait redressé QUE le nœud de la vue d'ensemble : la même
   grandeur est rendue à deux endroits, et le grep qui avait servi cherchait
   `pvPower.abs` — donc il montrait la ligne déjà corrigée et cachait l'autre.
   Un test couvre désormais le détail, en éprouvant l'ABSENCE du signe.

2. PAC et « Eau chaude » affichaient « non publié » alors que la box publie
   `{powerW: 0, source: "meter"}` pour les deux. `LoadConfigProvider.load()`
   n'était appelé que par l'écran des rôles : sur le tableau de bord la
   configuration est vide, aucun `domain` ne se rattache, et la carte reste sans
   valeur. Une absence CÔTÉ APP présentée comme une absence côté box — le défaut
   qu'on venait de corriger, reproduit un cran plus loin, et trompeur de la même
   façon : on en conclurait que la box ne publie rien.

Vérifié à l'écran entre les deux : « non publié » est lisible dans les cercles,
« Autres » a disparu, et le détail production ne montre plus d'onduleurs
fabriqués mais « Détail par source non publié par cette box ».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 13:24:53 +02:00
Patrick Schurig
c261b22c8c docs: l'étiquette d'honnêteté qui blanchit une fabrication — un mode d'érosion à part
Les silences qu'on traque se taisent ; celui-ci se DÉCLARE, et c'est ce qui le
protège. « PAC 1,5 kW » valait 38 % de la consommation du foyer, marqué « estimé »
et cerclé de pointillés : le nombre était faux, la mention était vraie, et la
mention a rendu le nombre acceptable assez longtemps pour survivre à l'arrivée de
vrais compteurs publiant zéro.

Le remède est le problème : marquer une invention la rend tolérable, donc pérenne.
Une valeur qu'on ne mesure pas ne se marque pas, elle ne s'affiche pas.

Avec les deux corollaires du lot : « non publié » plutôt qu'une part, et pas de
RESTE sans mesure exhaustive — un reste suppose de connaître la somme des parts.

Et la couche cachée, apportée par le moteur : `ECS-Meter` et `PAC-Meter` sont des
Things generic-energy appairés à la main, dont la puissance est SAISIE. Le total
dont l'écran tirait ses parts agrégeait donc déjà des valeurs tapées — deux
inventions superposées, dont la seconde est invisible depuis l'app, qui lit
`source: "meter"`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 13:10:49 +02:00
Patrick Schurig
12c3e912b0 fix(flux): les détails ne fabriquent plus de décomposition
Signalé sur banc, et les deux vues détaillées inventaient.

CONSOMMATION — PAC = 38 % de la maison, eau chaude = 18 %, autres = 22 %. Des
ratios posés à la maquette, marqués « estimé »… et affichés tels quels le
2026-09-06 alors que les compteurs ECS et PAC publiaient `{powerW: 0, source:
meter}`. Un compteur à zéro dit « rien ne chauffe » ; l'écran répondait 720 W.

La mention « estimé » était le problème autant que le remède : sous un nombre
inventé, elle le rend acceptable. Les branches viennent désormais de
`GetLoadTelemetry.measurement`, par domaine, et une charge sans mesure publiée
affiche « non publié ». « Autres » disparaît — c'était un reste calculé sur des
parts inventées, donc inventé lui aussi : sans mesure exhaustive, il n'y a pas de
reste connu.

PRODUCTION — « Onduleur 1 » et « Onduleur 2 » portaient chacun la MOITIÉ de la
production, avec l'état `ok`, donc sans même la marque « estimé » des branches de
consommation. Deux appareils qui n'existent nulle part : le banc n'a qu'un seul
compteur de production. La vue dit maintenant « détail par source non publié par
cette box ».

Et un débordement introduit en chemin, attrapé en test : « non publié » ne tient
pas dans un nœud dimensionné pour « 1.2 kW » — 7 px de trop, donc du texte coupé
sur l'appareil. `FittedBox(scaleDown)`, comme pour l'étiquette des tuiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 13:05:43 +02:00
Patrick Schurig
a14adcfcc3 fix(bilan): un cumul ABSENT n'est pas un cumul nul
`double d(String k) => (m[k] as num?)?.toDouble() ?? 0.0` : un champ manquant
devenait 0,0 au décodage, deux bornes à zéro donnaient une différence à zéro, et
l'écran affichait « 0,0 kWh · Vers réseau » comme une mesure. C'est le zéro forgé
que nous traquons depuis une semaine, cette fois de NOTRE côté.

Le cas est réel et n'attend pas sa cause : un compteur racine parfaitement en
place, la puissance instantanée qui arrive, et les cumuls qui n'arrivent pas.

`PowerBalanceEntry.totalsPresent` porte la distinction, et `fetchDailyBalance`
refuse de présenter comme mesure un bilan bâti là-dessus — au même titre qu'un
compteur corrompu. « Rien échangé aujourd'hui » et « la box ne publie pas ses
cumuls » sont deux états, et le second n'a pas le droit de porter le premier.
Le repli à 0 reste, pour ne pas propager des nullables dans tout l'écran Énergie,
mais il est désormais ACCOMPAGNÉ du fait qu'il est un repli.

Et `abs()` sur la production devient un choix DOCUMENTÉ plutôt qu'un correctif
tacite : la production n'est pas signée, c'est le compteur qui l'est, donc la
valeur absolue masquerait un compteur câblé à l'envers dont le signe serait la
seule trace. Le correctif appartient à la source. On la garde en connaissance de
cause — un signe faux se voit tous les jours, un câblage inversé ne se voit
jamais — et on ne revient pas en arrière sans savoir ce que la box publiera.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 12:41:10 +02:00
Patrick Schurig
a6dd319ef2 fix(rôles): le compteur RÉSEAU n'est plus détecté comme compteur solaire
Signalé sur banc après changement de matériel : Fronius SunSpec retiré, Eastron
SDM72 posé en rootMeter et SDM120 en compteur de production. L'écran affichait le
SDM72 dans les DEUX rôles, avec « auto · lecture seule » — donc sans moyen de le
corriger.

La box était juste (`GetRootMeter` rend bien le SDM72) et les interfaces
distinguaient les deux sans ambiguïté :

  Grid / sdm72          : energymeter, smartmeter, connectable
  PV   / sdm120Producer : smartmeterproducer, smartmeter, connectable

C'est notre liste qui ne discriminait pas : le rôle solaire acceptait
`energymeter`, `meter` et `smartmeter` — portées par TOUT compteur, réseau
compris — et ignorait `smartmeterproducer`, la seule qui dise « ce compteur
mesure une production ».

Deux corrections, la seconde tenant même si une future classe déclare des
interfaces trop larges :

- le rôle solaire exige désormais une interface de production ;
- le Thing acté comme compteur RACINE n'est plus candidat à aucun autre rôle.

Reste ouvert, et c'est le vrai fond du signalement : une détection automatique
ne devrait pas être un cul-de-sac. Les lignes de compteur n'ont aujourd'hui
AUCUN geste — pas de sélecteur, quel que soit l'état. Le badge « lecture seule »
est honnête, mais une inférence fausse doit pouvoir se corriger.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 12:31:59 +02:00
Patrick Schurig
b1c5336bbe docs: le test de fuseau porte sur le DÉCALAGE — ne pas le réutiliser pour un tarif
Europe/Berlin et Europe/Paris ne divergeront jamais : la ligne d'échéance restera
muette, et à raison — elle qualifie une horloge, et deux fuseaux au même décalage
donnent la même heure. Avertir parce que les NOMS diffèrent alors que les HEURES
coïncident serait le bruit que la condition existe pour éviter.

Mais une plage tarifaire suit le FOURNISSEUR, pas la box : « même décalage, autre
autorité » y reste une divergence réelle, et le jour où l'un des deux change de
règle d'heure d'été, la plage bouge sans que la box ait bougé. Le lot tarifaire
aura donc besoin d'une comparaison de provenance et d'un fuseau de référence
publié avec la configuration tarifaire.

Noté dans la doc du getter, là où quelqu'un ira le réutiliser.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 09:26:19 +02:00
Patrick Schurig
71bb26c81b feat(saisie): l'horloge se qualifie, elle ne s'offre pas en réglage
« 12:00 heure de la box (Europe/London) — pas celle de cet appareil », et non
« Fuseau : Europe/Paris » : sous un champ, un intitulé de réglage laisse croire
qu'on choisit le fuseau DE CETTE échéance. Il n'y en a qu'un, il est système, et
il déplace d'un coup toutes les heures murales de l'installation — échéances,
frontière de la journée de reporting, et demain les plages tarifaires
(LM-1005-b-iii).

Affichage CONDITIONNEL : seulement quand les deux horloges divergent. C'est la
règle déjà appliquée deux fois cette semaine — ligne des ratios instantanés,
avertissement « partiel » du bilan — et sa raison tient en une phrase : un texte
permanent cesse d'être lu le jour exact où il devient vrai.

La divergence se DÉDUIT de ce que la box publie, sans base de fuseaux :
`periodStartedAt` est minuit de l'installation en epoch, donc rendu dans le
fuseau de l'appareil il tombe sur 00:00 si et seulement si les deux partagent le
même décalage. Exact pour l'instant présent, y compris les jours à 23 ou 25
heures — et c'est justement pourquoi nous avions demandé cette frontière plutôt
qu'un nom IANA.

Quatre silences testés : fuseaux identiques, échéance vide, box qui ne publie pas
la date, et l'intitulé « Fuseau : » qui ne doit jamais apparaître.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 09:22:33 +02:00
Patrick Schurig
49e92b6b11 docs: état du banc laissé en place pour le week-end — rien écrit par l'app
L'obligation était déjà posée, et déjà à 2 000 Wh avant 12:00. État relevé à
08:55 pour que les traces de lundi soient lisibles.

Et une confirmation gratuite : `measuredSince` = 10:00 UTC = 12:00 Europe/Paris,
soit l'échéance d'hier — la base a bien été reprise à la bascule de période. Avec
`deliveredWh` = 31,4 kWh ≈ 21 h × 1 500 W, la fenêtre « hier 12:00 → maintenant »
se recoupe au watt près. Troisième invariant vérifiable de l'extérieur, après
Σ counts == targetW et gridW == remainingWh / remainingS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 08:56:37 +02:00
Patrick Schurig
42eef05c3b fix(carte): le parent domine, et la phrase éco cesse de se répéter
Deuxième passage sur appareil, écran des charges. Trois défauts, dont deux de
notre fait et un que les fixtures ne pouvaient pas produire.

1. LA FRACTION ÉTAIT ÉCRITE DEUX FOIS — `ECO_FLOOR_MET` porte déjà livré et
   cible dans son motif, et la ligne d'avancement les réécrivait dessous :
   « Plancher atteint — 29,9 kWh sur 3,0 kWh livrés » puis « 29,9 kWh sur 3,0
   kWh livrés · mesuré depuis 10:00 ». C'est le défaut de la ligne des ratios de
   la veille, refait à l'identique : déclencher sur ce qu'on SAIT plutôt que sur
   ce qui se VOIT. La ligne ne garde plus que ce que le motif ne dit pas.

2. « X SUR Y » N'EST PAS UNE FRACTION une fois le plancher dépassé — le compteur
   continue de courir après que l'obligation est tenue, et le numérateur finit à
   dix fois le dénominateur. Aucune fixture ne le montrait : toutes avaient un
   livré inférieur à la cible. Reformulé « 3,0 kWh dus, 30,2 kWh livrés ».

3. LE PARENT NE DOMINAIT PAS — à taille et graisse égales, l'allocation et les
   deux niveaux se lisaient comme trois lignes sœurs, et les pastilles colorées
   tiraient l'œil davantage que la commande qu'elles expliquent : le contresens
   exact que la forme doit empêcher (LM-1003). L'allocation passe en 13,5 gras,
   les pastilles perdent leur fond, et un filet vertical rattache chaque niveau
   à la commande.

Vérifié à l'écran après correction, et le repli l'était déjà : V2C et Simulated
wallbox tiennent sur une seule ligne, sans étiquette orpheline ni vide résiduel.

Et le signe du solaire est CONFIRMÉ, ce qui n'avait pas pu l'être hier faute de
production : la box publie -4 376 W, l'écran affiche « 4.4 kW ».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 08:11:22 +02:00
Patrick Schurig
b3000ca2d5 fix(écran): trois défauts trouvés sur appareil, dont un de forme et un de moi
Passage sur le Redmi contre `.75`. Aucun des trois n'était visible en test.

1. LA PRODUCTION S'AFFICHAIT NÉGATIVE — « -8.9 kW · Solaire » sous 9 kW de
   soleil. `currentPowerProduction` est publié négatif par nymea et le service
   garde les signes bruts par politique ; la carte imprimait la valeur telle
   quelle. Ce qui rend le défaut net plutôt qu'opinable : la même carte
   normalisait DÉJÀ le réseau et la batterie par `.abs()` — la branche solaire
   était la seule oubliée, donc un oubli et non une convention.

2. LE LIBELLÉ SE COUPAIT AU MILIEU DU MOT — « Autoconsommat / ion », alors que
   le commentaire du code disait vouloir l'éviter. La cause n'est pas la
   longueur mais l'ÉCHELLE du texte : police système au-dessus de 100 %, et
   `maxLines: 2` ne protège de rien puisque Flutter coupe un mot long sans
   espace, sans même afficher l'ellipse. `FittedBox(scaleDown)` garde le mot
   entier et ne fait rien quand la place suffit.

3. MA LIGNE DES RATIOS RÉPÉTAIT LES TUILES — « Instantané depuis 09:23 —
   autoconsommation 52 %, autonomie 54 % » sous des tuiles affichant 52 % et
   54 %. Les deux chemins étaient corrects et les valeurs coïncidaient vraiment
   (bilan 51,8/54,1 sur 00:00→16:45, box 51,9/53,7 depuis 09:23). Rien de faux,
   et illisible quand même : je déclenchais sur l'ORIGINE, alors qu'un lecteur
   compare des NOMBRES. La ligne sort désormais quand l'origine diffère ET que
   l'écart dépasse 2 points ; le relevé du jour est devenu un test.

Vérifié à l'écran après correction : le libellé tient sur une ligne, la ligne des
ratios se tait. Le signe, lui, N'A PAS pu être revu — la production du banc était
retombée à 0 W à 17:03, et `abs(0) == 0`. Une correction qu'on ne peut pas voir
demande une garde qui se voit : `flux_signe_test.dart` la pose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-05 17:07:20 +02:00
Patrick Schurig
763df68a2a feat(bilan): deux grandeurs, deux phrases — les ratios de la box à côté de la journée
Les tuiles disent la JOURNÉE, par différence de compteurs sur la fenêtre publiée.
La nouvelle ligne dit ce que la BOX publie, qui part de la baseline — et elle ne
s'affiche QUE lorsque cette origine diffère du début de journée.

Le cas est arrivé de lui-même : `nymead` redémarré à 09:23, sonde à 09:26, et la
box publiait 100 % d'autoconsommation. Vrai sur trois minutes ; absurde intitulé
« aujourd'hui ». La ligne le rend lisible — « Instantané depuis 09:23 » — au lieu
de le laisser passer pour la journée.

Vérifié avec le MODÈLE de l'app et non à l'œil sur le JSON : `ratios_check.dart`
importe `EnergyRatiosSnapshot` (idiome de `measurement_check.dart`) et rend
`baselineRepriseEnJournee: true`, ratios couvrant 3 minutes. Le banc est passé en
Europe/Paris entre-temps, et `periodStartedAt` suit sans que le code bouge —
c'est l'intérêt d'une frontière publiée.

Quand les deux coïncident, rien n'est ajouté : une phrase permanente cesse d'être
lue, comme un avertissement permanent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-05 09:31:44 +02:00
Patrick Schurig
bcf5b87e43 feat(bilan): la journée vient de la BOX — periodStartedAt, et non minuit du téléphone
Sondé avant de câbler (`+etm56`) : `periodStartedAt` = 1788562800 = 00:00 Londres
= 01:00 Paris, `measuredSince` = 07:04 Londres, `timeZone` = Europe/London. Sept
heures d'écart entre les deux bornes.

- `EnergyRatiosSnapshot` — les deux instants et le fuseau. `baselineRepriseEnJournee`
  dit quand les ratios de la box ne parlent que depuis un redémarrage : c'est le
  silence que `measuredSince` existe pour fermer, et quand les deux coïncident —
  le cas normal — il n'y a rien à dire de plus ;
- `fetchEnergyRatios()`, et `fetchDailyBalance()` qui prend désormais la frontière
  publiée. Repli sur minuit local pour une box antérieure, écrit comme tel.

Et un effet de bord attrapé avant qu'il ne parte : `startsLate` comparait à minuit
du TÉLÉPHONE. Avec la frontière de la box, il aurait crié « partiel » tous les
jours sur un bilan complet — un avertissement permanent cesse d'être lu. Il se
compare maintenant à `periodStart`, porté par le bilan, et un test fige le piège.

Les taux journaliers de `DailyBalance` sont donc corrigés du même geste : ils se
calculent déjà par différence de compteurs sur la fenêtre, c'était la fenêtre qui
était fausse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-05 09:20:31 +02:00
Patrick Schurig
cdffb9d804 feat(§10): l'avertissement de saisie — une consigne, pas une fatalité affichée
Lot configuration. La section « Obligation quotidienne » porte l'énergie due, son
échéance, et l'avertissement qui rend la déclaration honnête.

Pourquoi à la saisie : au résultat l'heure est passée, l'achat a eu lieu. Et sous
`unmeasurable` l'obligation ne peut JAMAIS être close — ni annoncée tenue (R6
interdit `ECO_FLOOR_MET`), ni chiffrée dans son manque (`missingWh` absent).
`ECO_FLOOR_MISSED` sort tous les jours par construction. Un mécanisme dont le
succès est inobservable et l'échec quotidien ne doit pas, en plus, dépenser en
silence.

Et c'est une consigne : les trois causes portent trois gestes — rattacher un
compteur (le plus fréquent, réparable), en mettre un qui totalise l'énergie, ou
constater que c'est le matériel. Une cause inconnue se dit sans inventer de
geste.

Le coût est CHIFFRÉ, pas estimé : le taux est constant depuis +etm55, donc le
total acheté vaut exactement la cible.

Trois silences délibérés, chacun testé : aucune obligation déclarée (personne ne
paiera), `progressMeasurable` absent (box antérieure à +etm49 — on ne conclut pas
d'une absence), et le nom du fuseau tant que l'app ne l'a pas lu. L'échéance dit
en revanche qu'elle est à l'heure de l'INSTALLATION, ce qui est vrai sans le
fuseau.

Revérifié à la sonde avant d'écrire, plutôt que cru : `progressMeasurable: true`
et `regime: measured` sortent bien sur la même charge — la contradiction du
2026-09-03 est fermée.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-05 08:40:35 +02:00
Patrick Schurig
58bf57e67f feat(§10): la carte de charge porte ses deux niveaux — une commande, deux lignes
Premier lot d'écran, sur des champs tous posés (+etm55) et tous vus à la sonde.
L'allocation reste le parent ; les niveaux en sont la décomposition. Une charge
reçoit UNE commande par cycle quelles que soient les passes (LM-1003) : deux
lignes décrivent une seule décision.

- `EcoProgress` — regime, targetWh, deliveredWh (absent sous `unmeasurable`,
  jamais 0), purchasedWh (publié dans TOUS les régimes : c'est la commande du
  moteur, donc un fait connu sans mesure), measuredSince, measuredGaps ;
- `LoadLevel` gagne `decision` et `progress` : chaque passe porte son motif.
  Lire la racine seule afficherait « rien fait » sur une charge qui achète —
  relevé du 2026-09-01 05:47, `SURPLUS_INSUFFICIENT` en racine et 767 W achetés
  au niveau éco ;
- `ecoDecisionText` / `ecoProgressText` : les phrases des trois motifs éco et de
  leurs quatre causes. Aucun nom de paramètre n'est deviné — `cause`,
  `missingWh`, les quatre valeurs et `measuredGaps` sont vérifiés dans le binaire
  posé. Seule la FORME de `measuredGaps` reste non observée, donc on n'en lit que
  la présence ;
- garde R6 à l'écran : `ECO_FLOOR_MET` sous `unmeasurable` est DÉNONCÉ comme une
  incohérence de la box, pas rendu. Servie oui, tenue jamais.

Deux interdits écrits dans le code, pas seulement dans la maquette : sous
`unmeasurable` on n'annonce pas que l'achat va s'intensifier (le taux est
constant depuis +etm55), et `atMaximum: false` n'y veut pas dire « on n'est pas
au plafond » mais « il n'y a aucun étalement, donc rien qui puisse cesser ». Un
booléen à false se lit spontanément comme rassurant.

Les charges utiles des tests viennent de `.75` — le cas éco-servi/confort-en-
attente du 2026-09-01, le cas mesuré du 2026-09-04 — pas d'une lecture de
contrat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-05 07:54:22 +02:00
Patrick Schurig
cd8dd02989 docs: la journée de reporting suit l'installation — et l'app prend celle du téléphone
+etm55 vérifié (posé le 04/09 18:21:08). Le taux constant est en ligne : la
maquette porte le bon chiffrage — ×4,75 sur une fenêtre quotidienne, facteur
ln(T·P/E) + 1 — et les deux interdits du régime `unmeasurable` : ni annoncer que
l'achat va s'intensifier, ni lire `atMaximum: false` comme « tout va bien », qui
signifie qu'il n'y a aucun étalement, donc rien qui puisse cesser.

La troisième nature d'heure murale touche du code livré : `fetchDailyBalance()`
et la baseline des ratios prennent minuit du TÉLÉPHONE, alors qu'une journée de
reporting suit l'installation. Sur `.75` en Europe/London vue d'un téléphone
français, entre minuit et 1 h l'app compte une journée que la box compte encore
dans la précédente.

L'app ne peut pas le corriger seule : `System.GetTime` publie un nom IANA, et un
décalage ne s'en déduit pas sans base de fuseaux — qu'il faudrait embarquer pour
recalculer une frontière que la box connaît, et qui resterait fausse les jours de
changement d'heure. Demandé au moteur : publier le DÉBUT de la journée de
reporting en cours, en epoch, à côté du fuseau. La divergence est écrite aux deux
endroits du code en attendant.

Noté sans conclure : le banc n'a aucune charge `unmeasurable`, donc la loi est
déployée mais pas exerçable de bout en bout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-05 07:40:26 +02:00
Patrick Schurig
0524e5f7cd docs: deux natures d'heure murale — une règle de stockage, pas d'affichage
État vérifié avant d'écrire : +etm53 sur la box, aucune clé de fuseau dans
`GetLoadConfig`, rien à câbler.

La distinction du moteur change ce qu'il faut PERSISTER, pas seulement afficher.
Le fuseau est une propriété du champ :

- une échéance client suit la maison — « 07:00 » nu est le bon stockage, et le
  réinterpréter dans le nouveau fuseau est le comportement juste : l'habitude
  suit le client ;
- une plage d'heures creuses est définie par le fournisseur — la réinterpréter
  parce que la box a bougé est un défaut silencieux, et rien dans la donnée ne
  permet de s'en apercevoir. Elle doit donc porter son fuseau de référence.

Et une demande : ne pas nous laisser coder `Europe/Paris` en dur. Un fournisseur
allemand définit ses plages en `Europe/Berlin` ; l'app choisirait une horloge à
la place de quelqu'un — le défaut d'aujourd'hui reproduit un cran plus haut.

L'avertissement de saisie compare deux choses différentes selon le champ :
installation vs appareil pour l'échéance, fournisseur vs installation pour la
plage tarifaire. Seul le second peut faire acheter au mauvais moment, et c'est
aussi le seul qu'un installateur ne peut pas deviner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-04 16:34:29 +02:00
Patrick Schurig
fdbabeb98a docs(§10): huit cycles, deux invariants, zéro écart — et l'acheté recoupe la consigne
Surveillance en lecture seule menée à son terme : livré 575 → 750 Wh (+25/min,
soit 1 500 W), reste 2 425 → 2 250, gridW 466 → 443 en décroissance monotone.
`deliveredWh + remainingWh == targetWh` et `gridW == remainingWh / remainingS`
vérifiés 8 fois sur 8, aucun échec.

Troisième recoupement, celui qui permet à l'écran de chiffrer le coût sans
l'inventer : `purchasedWh` gagne 53,2 Wh en sept minutes — 456 W de moyenne —
contre 454,6 W de moyenne pour `gridW` sur les mêmes cycles, 0,3 % d'écart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-04 07:56:27 +02:00
Patrick Schurig
1d679312c5 docs(§10): la baisse mesurée, et l'échéance est à l'heure de la BOX
Sonde en LECTURE SEULE — l'obligation est celle du moteur, rien n'a été écrit.

Le critère positif est atteint : `deliveredWh` +25 Wh/min (1 500 W, ce que le
compteur ECS annonce), `remainingWh` qui descend d'autant, `gridW` 466 → 463.
Décroissance, pas absence de mur.

Deux invariants tiennent, et l'écran les vérifiera à chaque trame :
- `deliveredWh + remainingWh == targetWh` — et c'est sous `measured` qu'elle
  mord, là où elle n'est plus le corollaire trivial d'`unmeasurable` ;
- `gridW == remainingWh / remainingS` — 466 == 466 puis 463 == 463, donc la loi
  d'étalement est vérifiable de l'extérieur, sans lire le code.

Fuseau : `remainingS` le confirme de notre côté (18 719 s à 06:48 heure box
mènent à 12:00 heure box), et `System.GetTime` publie `timeZone` — l'app peut
donc être honnête sans attendre l'arbitrage. Règle écrite dans CLAUDE.md et dans
la maquette : dire l'horloge au lieu de la choisir, fuseau de la box affiché à
côté du champ et divergence signalée. Ce que le lot moteur doit trancher reste
entier : d'où vient le fuseau, et ce qu'il fait quand il diverge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-04 07:50:41 +02:00
0564c47569 docs(AGENTS): dater le paquet qui porte la grandeur, pas celui qui porte la fonction
Report de la règle déjà consignée dans CLAUDE.md (§ Règles de modification),
avec la ligne complétée du second encodage : une chaîne QStringLiteral est
stockée en UTF-16 et reste invisible à un `strings` nu.
2026-09-04 07:13:43 +02:00
Patrick Schurig
49ec3dc75c docs: vérifier le paquet qui possède la GRANDEUR, pas celui qui porte la fonction
+etm53 était bien posé (05:08:41) et l'étalement adaptatif restait inobservable :
`ECS-Meter.totalEnergyConsumed` figé à 10, deux lectures à une seconde d'écart.
La cause n'est pas dans le moteur — la classe `smartMeterConsumer` appartient à
`nymea-plugin-generic-energy`, version 1.15.2+202606191336~trixie1, `.so` daté du
2026-06-19 et jamais reconstruit.

Notre règle de vérification (`dpkg -l | grep powersync-energy`) aurait répondu
« posé, tout va bien » et nous aurait fait attribuer au moteur le silence d'un
paquet qu'on ne lui avait pas livré. La fonction et la grandeur qu'elle consomme
peuvent vivre dans deux paquets différents ; c'est la seconde qu'il faut dater.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-04 06:49:53 +02:00
Patrick Schurig
bf23bda070 docs(§10): +etm52 — la contradiction progressMeasurable/regime est levée, mesurée
Sonde sur le banc, version relue avant (1.15.2+etm52, posée le 03/09), obligation
déclarée puis retirée, restauration « 0 écart, clé par clé ».

`progressMeasurable: true` et `regime: "measured"` sortent enfin sur la même
charge : l'avertissement de saisie parlera là où il faut, et l'achat à l'aveugle
a cessé pour une charge instrumentée. `deliveredWh` est présent (0, pas absent),
`measuredSince` stable sur sept cycles.

`purchasedWh` et `deliveredWh` divergent — 27,5 Wh achetés, 0 livré — et c'est ce
qui prouve qu'ils ne font pas doublon. Ici la cause est bénigne :
`ECS-Meter.totalEnergyConsumed` vaut 10 depuis le 1er septembre, constante du
mock. Mais la divergence donne à l'écran un contrôle qu'il n'aurait pas su
fabriquer : sous `measured`, acheté qui monte et livré qui reste plat = soit la
charge ne consomme pas, soit le compteur ne compte pas.

Non conclu : la décroissance de `remainingWh`, donc l'étalement adaptatif. Elle
exige un cumul qui avance. Le comportement observé est aujourd'hui identique à
celui d'`unmeasurable` pour une raison entièrement différente — la coïncidence
même qui ferait conclure juste sur une mauvaise raison.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-04 05:47:40 +02:00
Patrick Schurig
e4697980ad docs(§10): la garde de sessionWh doit porter sur la joignabilité, pas sur le recul
Mise en garde avant que le remplissage de `sessionWh` depuis le compteur soit
écrit, et elle vient d'un relevé déjà pris.

Le « recul franc » de `getEcoProgress()` recapture la base pour une nouvelle
session. Mais un compteur qui disparaît peut publier 0 au lieu de se taire —
mesuré sur la V2C le 2026-08-30, `currentPower` 4 396 → 0 et les booléens à
`false` à la perte du lien. Les deux cas donnent la même observation et appellent
des gestes opposés.

Et le coût est asymétrique : base reprise à 0 pendant l'absence, retour du
compteur avec son cumul réel (10 kWh sur l'ECS), et `deliveredWh` saute à 10 000
en un cycle — l'obligation se déclare TENUE sans rien avoir livré. Sous
`measured`, R6 ne s'y oppose pas : elle n'interdit `ECO_FLOOR_MET` que sous
`unmeasurable`. Le chemin mesuré est le seul par lequel un faux « tenue » peut
sortir, et c'est celui qu'on ouvre.

La garde doit donc porter sur la joignabilité du compteur, pas sur la direction
de la valeur — même règle que la clôture de tronçon, et même geste que l'app a
fait le 2026-08-30 en jetant les états d'un Thing injoignable à l'entrée.

Portée honnête : le zéro forgé a été mesuré sur `currentPower` et les booléens,
pas sur un compteur d'énergie — `sessionEnergy` de la V2C est resté à sa dernière
valeur. La politique dépend du plugin, d'où l'intérêt de ne pas parier dessus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-03 07:01:10 +02:00
Patrick Schurig
78f7fa23a9 docs(§10): +etm49 — purchasedWh marche, et deux champs se contredisent
Sonde sur le banc (obligation déclarée puis retirée, restauration « 0 écart »).

Vérifié : les cinq symboles dans le binaire déployé, tous en UTF-16 ;
`progressMeasurable` bien dans `GetLoadConfig` en `r:` ;
`progressUnmeasurableCause` absent quand il n'y a pas de cause ; et
`purchasedWh` qui accumule SOUS `unmeasurable` — 0 → 43,12 Wh, ~648 W, cohérent
avec `gridW`, donc recoupable par l'écran.

Mais `progressMeasurable: true` et `regime: "unmeasurable"` sortent sur la MÊME
charge au même cycle, et le code dit pourquoi : `progressMesurable()` teste ce
que le MATÉRIEL permet (compteur désigné qui compte l'énergie), tandis que
`sessionMeasurable` — qui décide du régime — n'est posé que dans `evadapter.cpp`.
Sur toute charge non-VE, l'un dit oui et l'autre non.

Conséquence : l'avertissement de saisie, qui se déclenche sur
`progressMeasurable == false`, restera muet exactement sur les charges achetées à
l'aveugle. Nous ne le câblons pas sur ce champ, et l'app ne peut pas rattraper —
à la saisie la charge n'est pas dans `loads[]`, donc son régime n'existe pas.

Statut sans reproche : l'escalade est toujours là en +etm49 (`remainingWh` figé,
`gridW` 645 → 654). Le taux constant est retenu, pas déployé.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-03 06:54:45 +02:00
Patrick Schurig
5695ab8928 docs(§10): bascule de régime — prémisse vérifiée, champs non déployés, perte trouvée
Ma prémisse (« la base est capturée à la bascule, targetWh n'est pas rebasée »)
était juste mais posée sans vérification : confirmée par lecture de
`energyarbitrator.cpp` et `ecotarget.cpp`.

Le moteur a déjà tranché, et mieux que ma question : `measuredSince` plutôt que
rebaser `targetWh`, parce que rebaser ferait perdre l'obligation réelle et
l'écran ne pourrait plus dire ce qui reste dû. `purchasedWh` est là aussi, dans
tous les régimes.

MAIS aucun des deux n'est sur la box : `strings -el` et `strings` sur le binaire
déployé trouvent `ECO_FLOOR_GRID`, `ECO_FLOOR_MISSED`, `remainingWh` et
`atMaximum` — et zéro occurrence de `purchasedWh` et `measuredSince`. Ils sont
au HEAD, pas dans +etm43. Rien à câbler dessus aujourd'hui.

Et une perte que la lecture montre : sur `measured → unmeasurable → measured`
dans la même période, la base et la date sont retirées puis reprises à la valeur
courante, donc `deliveredWh` repart de zéro et les tronçons mesurés antérieurs
sont perdus. L'écran sous-estime, et la passe éco rachète ce qui a déjà été
livré. Le lien Modbus battant de la V2C produit exactement ce motif.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-02 07:55:32 +02:00
Patrick Schurig
5597c530fc docs(§10): trois champs pour que l'écran tienne la nuance « non vérifiable »
Les deux règles du moteur sont prises. Il leur manque trois choses pour être
affichables :

1. `ECO_FLOOR_MISSED` sous `unmeasurable` — la cause porte la nuance et
   `missingWh` absent est juste, mais le COÛT manque. Sous le taux constant le
   moteur aura acheté exactement la cible et il le sait exactement : il l'a
   commandé. `draw.committedW` est une puissance par cycle, pas un cumul.
   `purchasedWh` est légitime là où `deliveredWh` ne l'est pas — l'un est une
   comptabilité, l'autre une mesure absente. Sans lui, la règle « ce que ça coûte
   par jour » ne peut être qu'une estimation de l'app.
2. Le signal de déclaration doit vivre dans `GetLoadConfig` en lecture seule, pas
   dans la télémétrie : `measurement` n'existe que pour les charges de `loads[]`,
   et une charge qu'on déclare est justement celle qui n'y est pas. Avec sa
   raison — aucun compteur / compteur sans énergie / borne sans `sessionEnergy` —
   parce que le geste de l'installateur en dépend. `meterThingId` ne suffit pas :
   un compteur peut publier la puissance sans l'énergie.
3. Après une bascule `unmeasurable → measured`, `deliveredWh` compte depuis la
   nouvelle base pendant que `targetWh` reste la cible entière : la paire cesse
   d'être une fraction, et une barre afficherait « 0,5 sur 4,0 » après une
   bascule où 3 kWh ont pu être achetés. Soit `targetWh` est rebasée, soit un
   champ dit depuis quand on mesure. Aucune barre avant que ce soit tranché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-01 07:55:47 +02:00
Patrick Schurig
ff18dfee48 docs(§10): acheter sans vérifier — figer le TAUX, pas le reste
Réponse à la question du moteur. L'escalade de l'étalement est une correction
calculée sur une grandeur qui n'existe pas sous `unmeasurable` : `remainingWh`
étant figé par contrat, le taux ne suit plus un retard mesuré, il suit l'horloge.

Et ce n'est pas qu'un risque de mur — la loi surachète sans borne : ∫ E/(T−t) dt
diverge, bornée seulement par le plafond du mécanisme. Chiffré sur notre relevé
(4 000 Wh, 18 779 s, plafond 3 500 W) : plafond atteint 1 h 09 avant l'échéance,
total ≈ 10,1 kWh pour 4,0 kWh dus — x2,5, et le facteur grandit avec la fenêtre.

Proposition : remplir `sessionWh` depuis le compteur de charge d'abord (le vrai
correctif, LM-1014-b s'y applique telle quelle) ; puis, pour ce qui reste non
mesurable, taux CONSTANT = cible ÷ fenêtre à l'ouverture, `atMaximum` jamais
vrai — le total acheté vaut exactement la cible.

La part qui nous revient : la déclaration d'une obligation sur une charge sans
mesure d'énergie devra dire, à la saisie, qu'elle sera achetée à l'aveugle, ce
qu'elle coûte par jour, et qu'elle ne pourra jamais être annoncée tenue.

`targetW`/`counts` pour l'arithmétique, `gridW` pour la phrase : confirmé.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-01 07:53:13 +02:00
Patrick Schurig
def985d3a0 docs(§10): les six champs vus à la sonde — et la trajectoire qu'ils révèlent
Obligation déclarée sur le banc puis retirée (`chauffe-eau`, 4 000 Wh avant
12:00, +etm43, sept cycles). Capture sur disque avant la première écriture,
restauration DEPUIS le fichier, comparaison clé par clé : 0 écart.

La forme est celle annoncée, et elle porte plus que le motif : `decision` et
`progress` descendent au niveau, `deliveredWh` est absent sous `unmeasurable`,
`counts` tombe sur `draw.committedW`, et le motif racine porte `level`.
R1, R5 et R7 vérifiés sur le fil ; les deux identités tiennent à chaque cycle.

Deux choses que la mesure apprend, et qu'aucune lecture de code n'aurait données :

- au cycle 05:47 le surplus passe à −922 W : `comfort` tombe à 0 avec
  `SURPLUS_INSUFFICIENT` pendant que `eco` achète 767 W au réseau. Le motif
  RACINE vaut alors `SURPLUS_INSUFFICIENT` — un client d'avant `levels[]` lit
  donc « rien fait » sur une charge qui achète. Vue fausse, pas incomplète ;
- sous `unmeasurable`, `remainingWh` ne bouge pas et `remainingS` descend, donc
  `gridW` monte (755 → 767 en quatre cycles). Le plancher ne peut jamais se
  refermer : l'accélération jusqu'au mur est la trajectoire GARANTIE du régime,
  pas son cas limite. La maquette affiche désormais reste + temps, et pas
  seulement les watts de l'instant.

Question posée au moteur : la charge a un compteur (`totalEnergyConsumed: 10`)
et le régime sort quand même `unmeasurable`. Faut-il remplir `sessionWh` depuis
le compteur, et faut-il acheter quand on ne peut pas vérifier ?

`remainingWh` plutôt que `deliveredWh` est accepté, et R5 porte l'argument :
`remainingWh` n'est pas dérivable sous `unmeasurable`, qui est justement le
régime où l'on achète.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-01 07:48:39 +02:00
Patrick Schurig
7eefb93c6c docs: relevé frais sur charge réelle — sessionEnergy corrigé, chargingState confirmé
Lien Modbus rétabli, Skoda en charge, valeurs en mouvement.

- `sessionEnergy` : notre doc était EN RETARD, pas contredite. La V2C la publie
  depuis le plugin +etm13 et elle bouge (15,8015 → 15,8405 kWh). L'écart avec
  `chargeEnergy` vaut 7,2044 kWh aux deux échantillons pris à deux heures
  d'écart — constant au dix-millième : c'est l'accumulateur du plugin, et il
  prouve l'accumulation que le code seul ne montrait pas. LM-1009 C1 reste vraie
  pour les bornes sans cet état ; la V2C n'en fait plus partie. La maquette ne la
  nomme donc plus dans l'exemple « non mesurable ».
- `chargingState` reste `Idle` sur un lien VIVANT, pendant que la borne tire
  4 372 W : ce n'est pas de la péremption, personne ne maintient le champ.

Et un constat qui n'est pas dû au WiFi : la box a journalisé « Restoring manual
values of false 6 A » trois secondes après la coupure ; la borne est restée à
`power: true`, 7 A, et le lien est revenu sans que rien ne réapplique. En mode
manuel personne ne commande, donc l'écart est permanent — 4,4 kW tirés,
`draw.committedW` à 0. Question posée au moteur.

Aucun avancement n'est affiché pour autant : c'est une décision (D1), pas une
mesure qui manquerait.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 15:15:49 +02:00
Patrick Schurig
2c9cca042e fix(borne): une borne injoignable ne montre ni le zéro forgé ni la valeur périmée
Mesuré sur `.75` le 2026-08-30 : quand le lien Modbus de la V2C tombe, la box ne
fige pas les états, elle publie des zéros — `pluggedIn` false, `charging` false,
`currentPower` 0 — alors que la Skoda était branchée et en charge à 4 356 W.
Avant ça, la dernière valeur connue reste publiée comme si elle était actuelle
(vérifié : aucun état ne bouge en 45 s pendant une charge supposée).

L'app refusait déjà d'inventer un zéro ; elle en relayait un. Sous
`connected == false`, `EvChargerLive` met `pluggedIn`, `charging` et
`currentPowerW` à `null` À L'ENTRÉE — un seul endroit, pas un test par écran — et
porte `joignable` + `messageEtat`. La tuile dit « Hors ligne » et reprend la
phrase du plugin, qui porte l'heure de la coupure et la piste WiFi.

Deux garde-fous dans les tests : `joignable == null` n'est PAS hors ligne (une
borne qui ne publie pas `connected` ne dit rien de son lien — supposer la panne
serait le symétrique du zéro forgé), et le MODE reste affiché hors ligne, parce
qu'il est stocké dans la box et non lu sur la borne.

Brief moteur : les zéros à la perte du lien, `chargingState` figé à `Idle`
pendant une charge réelle, et `ChargingModeNormal` qui n'est pas un boost — il
restaure les valeurs manuelles mémorisées (ici arrêt, 6 A) et sort la borne de
l'arbitrage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 13:43:31 +02:00
Patrick Schurig
a1ffd13107 feat(télémétrie): la réconciliation repose sur counts — l'exception EV_GRID_START tombe
`+etm32` publie `counts` par niveau : où chaque watt est compté, et combien.
Vérifié à la sonde sur `.75` avant de toucher au code — `counts` sur tous les
niveaux, destination publiée même à zéro, et les DEUX identités qui tiennent.

- `LoadLevel.counts`, `countsTotalW`, `countsCoherent`, `partSurplusW` ;
- `surplusShareW` LIT `counts["budget.allocatedW"]` au lieu d'interpréter ;
- `soutirageShareW` + `LoadTelemetry.soutirageIdentityHolds` : la seconde
  identité, qui n'existait pas ce matin faute de registre pour les watts achetés ;
- `LoadTelemetry.drawCommittedW` — absent = box antérieure à `+etm31`, jamais
  « rien acheté » ;
- `destinationsInconnues` : une clé née après cette app est NOMMÉE, jamais
  comptée zéro, et l'identité par niveau tient sans connaître le sens des clés.

L'exception au motif survit exactement là où la box ne sait pas dire le partage
autrement — formes sans `counts`. Ce n'est pas une version qui l'a retirée, c'est
une charge utile qui l'a rendue inutile ; sur une installation déjà déployée, la
retirer casserait la réconciliation.

Le rapport de bogue porte désormais les deux identités, les destinations
inconnues et les niveaux incohérents (Σ counts != targetW) — une incohérence de
la box, pas un arrondi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 10:51:10 +02:00
Patrick Schurig
47866ddee3 docs(sondes): défaire son action n'est pas restaurer l'état
Payé le 2026-08-30 en cherchant à provoquer EV_GRID_START. Trois erreurs, aucune
n'a produit de code d'erreur :

- `connect` bascule `connected` (la liaison du Thing), pas `pluggedIn` — lecture
  seule, qu'aucune action n'écrit. La borne n'est jamais entrée dans `loads[]` ;
- le `finally` défaisait le GESTE, pas l'ÉTAT : `connected` valait déjà `true`
  avant, donc la « restauration » a laissé la borne déconnectée dix minutes ;
- `ThingErrorNoError` est un VRAI succès, et c'est ce qui rend le cas plus
  vicieux que `status: "success"` : l'action a réussi, elle ne faisait pas ce
  qu'on croyait. Aucun code ne dira « tu t'es trompé d'action » — le seul verdict
  est l'état relu.

La règle que `fault_probe.dart` tenait déjà pour la configuration est généralisée
à `ExecuteAction` : capturer avant, restaurer VERS la capture, comparer état par
état.

Et le fait mesuré, pour qui réessaiera : EV_GRID_START n'est pas provocable
depuis la frontière RPC (pas de prise véhicule exposée, pas d'`assignedCarId`).
La fenêtre, elle, s'ouvre seule — 07:46, surplus 905 W, sous le minimum de la
borne. La preuve du partage viendra donc de la suite de simulation du moteur ;
le critère ne bouge pas, et la branche reste jusqu'à sa trace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 10:18:36 +02:00
Patrick Schurig
820e58c13e docs(sonde): +etm31 vérifié — draw.committedW est là, l'exception reste
Sonde contre `.75`, 2026-08-30, `powersync-energy-plugin-nymea 1.15.2+etm31`
confirmé par dpkg. Trois points sur quatre vérifiés :

- `draw` dans la MÊME trame que `budget`, **une seule clé** `committedW` —
  `authorisedW`, `remainingW`, `binding`, `perPhaseBound` absents, pas à zéro ;
- `draw` publié même quand rien n'est acheté (`committedW: 0`, deux cycles) —
  donc son ABSENCE vaudra « box antérieure », jamais « rien acheté ce cycle ».
  Zéro est une valeur quand la grandeur existe, l'absence quand elle n'existe
  pas ;
- identité surplus tenue : 1 200 == `budget.allocatedW`.

Le quatrième ne l'est pas, et c'est celui qui décide : `EV_GRID_START` est
**inobservable sur le banc**. Les deux bornes sont déclarées dans
`GetLoadConfig` (V2C Trydan, Simulated wallbox) et absentes de `loads[]` — hors
arbitrage, rien de branché — donc le motif ne peut pas survenir.

La branche de `surplusShareW` reste. Elle ne partira qu'à l'observation d'un
`EV_GRID_START` réel et de ses deux moitiés dans `budget.allocatedW` et
`draw.committedW`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 09:30:45 +02:00
Patrick Schurig
faffc43dd9 docs: draw.committedW n'est pas sur la box — annoncé ≠ construit ≠ posé
CLAUDE.md le portait comme « annoncé livré, non vérifié ». Le fait est plus
simple : le code est écrit côté moteur, le cross-build n'a pas été fait, aucun
paquet +etm31 n'existe, la box porte +etm30 et la trame ne contient aucune clé
`draw`. La pose est relancée ; l'exception `EV_GRID_START` tient jusqu'à ce que
la sonde voie le champ sortir.

Et la règle qui a servi entre dans les règles de modification, à côté de celle
sur `JSONRPC.Introspect` : vérifier la VERSION POSÉE avant de croire une
livraison annoncée — `ssh etm@192.168.1.75 'dpkg -l | grep powersync-energy'`.
Un champ absent parce qu'il n'est pas déployé se lit exactement comme un champ
absent parce qu'il est omis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 09:25:02 +02:00
Patrick Schurig
43fd79ad62 feat(écran): « Commandé, pas encore mesuré » remplace « Réservé au véhicule »
Le moteur a livré la définition d'`evReservedW`, et il ne pouvait pas l'écrire
avant d'en avoir sorti la part réseau d'`EV_GRID_START` : un champ à deux natures
ne se documente pas. Le libellé tranché est le sien, parce qu'il dit la chose —
la latence entre l'ordre et sa lecture au compteur.

« Réservé au véhicule » affirmait deux faussetés : que c'est réservé, et que ça
regarde le véhicule plutôt que la latence de sa mesure.

La clé ARB garde le nom du champ de la box (`budgetEvReserved`), pas celui de
l'ancienne affirmation. Et la consigne qui compte reste écrite à côté : ce n'est
pas un registre, on ne réconcilie rien dessus — la nature du champ l'interdit,
pas son état de livraison.

`draw.committedW` : annoncé livré en +etm31, NON vérifié à la sonde. La box porte
`powersync-energy-plugin-nymea 1.15.2+etm30` au 2026-08-30 et ne publie aucune
clé `draw`. L'exception `EV_GRID_START` reste donc dans `surplusShareW` : c'est
la sonde qui autorise le retrait, pas l'annonce.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 09:16:04 +02:00
Patrick Schurig
b01a716047 docs(R3): draw.committedW en préalable — et evReservedW n'est PAS un registre
Le moteur l'a trouvé en câblant `counts` : `budget.evReservedW` est un terme de
CORRECTION du budget, pas un registre d'allocation. Il mêle la correction
« commandé non encore mesuré » à la part réseau d'`EV_GRID_START`, et une borne
servie au réseau qui tire déjà ce qu'on lui a commandé y contribue 0 pour un
`targetW` de 7 000 W. `Σ counts == targetW` y serait vraie au premier cycle après
un changement et fausse ensuite : une identité dépendante du temps, qui passe
tous les tests et certifie un total faux. Ne pas livrer était le bon appel.

Réponse : `draw.committedW` en préalable, oui, sans réserve — et trois demandes :
par cycle, en watts, dans la même trame que `budget` ; publié SEUL, les quatre
autres champs de `draw` absents et jamais à 0 (un `authorisedW` forgé afficherait
un plafond de soutirage qui n'existe pas) ; et une définition d'`evReservedW`,
que nous affichons sous « Réservé au véhicule » — un libellé qui affirme une
réservation là où il y a une correction, et que nous ne renommerons pas au jugé.

Nous portions la fausse affirmation en cinq endroits (`CLAUDE.md` ×2, le modèle
×3, la description ARB) : retirée ici, sans changement de comportement. Elle ne
relève pas de la discipline « attendre la sonde » — celle-ci arbitre ce que fait
la box, pas ce que nous en avions cru, et la phrase est fausse aujourd'hui.

Il n'existe donc qu'UNE identité vérifiable, et elle ne couvre qu'un côté :
Σ targetW(surplus) == budget.allocatedW. Côté réseau, rien — pas « à vérifier
plus tard », rien, jusqu'à `draw.committedW`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 08:35:17 +02:00
Patrick Schurig
1d0112a6e4 docs(R3): counts retenu — et sur un niveau mixte, ni omission ni « mixed » publié
Arbitrage rendu au moteur avant qu'il n'écrive R3. La forme proposée est prise
telle quelle : `funding` reste, `counts` s'ajoute, `Σ counts == targetW`
toujours, publié même à une seule destination. L'exception `EV_GRID_START`
disparaît du contrat, remplacée par une règle vérifiable par niveau.

Trois apports de notre côté :

- « approximation » est le mauvais mot et coûterait le champ : quelqu'un
  voudrait le rendre exact, et un libellé par part dominante basculerait d'un
  cycle à l'autre sur 900/480. Règle demandée : **`grid` l'emporte dès qu'un
  watt est acheté** — déterministe, et vraie sans nouvelle décision pour le
  plancher éco du §10 ;
- `"mixed"` publié est refusé, et c'est notre propre code qui le dit : une
  troisième valeur tombe dans la branche par défaut de tout client — « surplus »
  — donc reproduit la panne de l'omission, avec un champ présent en plus pour
  faire croire le contraire. Et il serait dérivable de `counts`, donc supprimé
  un jour à juste titre. Notre `mixed` reste dérivé, local, jamais transporté ;
- ce que l'app promet : clés de `counts` traitées comme identifiants opaques,
  une clé inconnue dit « destination inconnue de cette version », jamais zéro.

Contrat, pas feuille de route : `CLAUDE.md` et `surplusShareW` gardent
l'exception écrite tant que la sonde ne l'a pas vue disparaître de la box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 08:24:16 +02:00
Patrick Schurig
5c06e65a0e docs(télémétrie): allocatedW publie le DÉCIDÉ, pas le commandé
La prémisse inverse était écrite dans CLAUDE.md et dans le modèle, et c'est elle
qui a produit l'erreur de la maquette §10 : un écran cherchant l'arrondi entre
`Σ targetW` et `allocatedW` compare deux valeurs égales par construction — il ne
montre jamais rien, et sans erreur.

Trois nombres nommés à leur place : décidé (`allocatedW`), appliqué
(`mechanism.*`), mesuré (`measurement.powerW`) — et ce que chaque écart prouve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 08:17:01 +02:00
Patrick Schurig
2792e07265 fix(télémétrie): funding a quitté la charge en +etm30 — le lire dans levels[]
R2 est livrée côté moteur (1.15.2+etm30 sur .75) : `funding` ne figure plus à la
racine d'une entrée de `loads[]`, il vit dans `levels[]`, un par passe. La rupture
est effective, pas seulement contractuelle — `entry.funding` est désormais `null`
partout, donc la mention « servie au réseau » avait disparu de la carte et une
charge achetée au réseau aurait été comptée comme surplus dans l'identité. Une
panne muette : rien n'échoue, tout se lit de travers.

- `LoadLevel` — une passe : `level`, `targetW` (du DÉCIDÉ, comme `allocatedW`),
  `funding` ;
- `resolvedFunding` / `isGridFunded` résolvent les DEUX formes ; `levels[]` vide
  = box antérieure, pas « aucune passe » ;
- `'mixed'` est NOMMÉ plutôt que replié : impossible aujourd'hui (une seule passe
  publiée), courant au §10, et l'affirmer d'un côté serait une demi-vérité ;
- `surplusShareW` somme les `targetW` financés au surplus, l'exception écrite
  `EV_GRID_START` (part de surplus = `params.budgetW`) passant avant.

Vérifié à la sonde contre `.75`, pas en test : 3 500 + 3 000 == 6 500 ==
`budget.allocatedW` au cycle 06:10, puis 1 500 == 1 500 à 06:14. Le niveau publié
est « comfort », et c'est déjà le bon nom.

Aucun écran à deux niveaux n'est commencé : le reste de la maquette attend R1 et
R3 à R8, et le §10 n'est pas implémenté.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 08:16:29 +02:00
Patrick Schurig
6f19863620 docs(§10): réponse au §9 — deux passes, une commande, et la forme qui la publie
La maquette `docs/mockups/deux_passes_eco_confort_mockup.html` répond aux trois
questions du brief plugin §9 (= DESIGN_10 8.5), et sert de contrat au moteur.

- la liste reste ordonnée par RANG : l'ordre de service n'est pas une propriété
  de la configuration — il change d'un cycle à l'autre selon les planchers déjà
  atteints, donc une liste triée par lui bougerait sous le doigt ;
- une entrée par charge, deux lignes dans la carte : on dédouble là où l'objet
  affiché est la SÉQUENCE, jamais là où c'est la CHARGE — d'où un second écran,
  « ordre de service du cycle », en lecture seule ;
- « éco servi / confort non » s'affiche en deux états NOMMÉS : en fraction, le
  régime nominal se lirait comme une dégradation permanente.

Huit exigences pour `GetLoadTelemetry`, dont R2 (`funding` descend au niveau) et
R4, dont la prémisse a été corrigée par le moteur : `allocatedW` publie le
DÉCIDÉ, donc `Σ targetW == allocatedW` est une identité, et l'écart à afficher
vit entre le décidé et `mechanism.*` (appliqué). D'où le bloc « trois nombres ».

Contrat, pas feuille de route : aucun écran ne se construit avant `levels[]`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 08:10:31 +02:00