25 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
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
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
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
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
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
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
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
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
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
e362fb7430 chore(telemetry): l'alias measuredW est retiré — +etm26 est sur .75
Vérifié par sonde avant de couper : `measuredW` a disparu de la charge utile,
`measurement` est en ligne. L'alias n'aura vécu que le temps de l'intervalle qu'il devait
couvrir, et `MeasurementSource.legacy` part avec lui.

Ce qui reste, `MeasurementSource.inconnue`, ne le remplace pas : il couvre une box PLUS
RÉCENTE que l'app — la même règle que partout ailleurs dans ce fichier, un code inconnu
tombe sur un repli lisible plutôt que sur une phrase inventée. On affiche la valeur, on ne
prétend rien sur ce qu'un écart prouverait.

── La garantie qui manquait à ce que j'avais écrit ──────────────────────────

`measurement` est présent pour toute charge de `loads[]`, et pour elle seule. C'est elle
qui rend la lecture sûre, et elle est maintenant au contrat : son absence ne peut PAS
vouloir dire « pas mesurable » — ça, c'est `none`, un état publié — elle ne peut vouloir
dire que « charge hors de loads[] », cas où l'on ne conclut rien.

J'avais fait la distinction par prudence sur l'écran ; elle est désormais fondée sur une
garantie, pas sur une intuition.

── Les sources du banc ne sont pas celles annoncées, et c'est instructif ────

  chauffe-eau   meter, 1 500 W   porte ECS-Meter
  pac-terrain   meter,   800 W   porte PAC-Meter — pas `none`
  V2C Trydan    absente de loads[]  (pluggedIn: false)

`pac-terrain` en `meter` ne contredit pas LM-1105-b : cette règle décrit ce qu'une PAC
publie SANS compteur dédié. On lui en a posé un — ce que le brief recommandait — et le
compteur explicite l'emporte toujours. Et la V2C illustre la distinction sur machine :
absente, pas `none`. Elle porte `currentPower` et publiera sa mesure au cycle où une
voiture y sera branchée.

⚠️ Le test sur appareil exigeait `measuredW == null` après détachement du compteur.
C'était vrai avant `+etm26`, c'est faux depuis : le moteur retombe sur l'appareil quand il
publie lui-même sa puissance. Ce qui doit cesser, c'est que la source vaille `meter` sans
compteur désigné — pas que la mesure disparaisse.

CLAUDE.md porte le contrat complet et les deux pièges, avec l'illustration du banc.

205 tests, flutter analyze inchangé à 27 remarques.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
2026-08-28 15:32:02 +02:00
Patrick Schurig
f33b8c243a feat(support): le rapport de bug pré-rempli, repoussé depuis le premier jour
/bug-report était un stub. Un champ libre seul produit « ça ne marche pas », et l'aller-retour
pour obtenir versions, charges et état d'arbitrage coûte des jours. Tout ce que l'app SAIT
est donc joint d'office : version du paquet, hôte et état de connexion, ligne de versions du
schéma, les charges déclarées avec adaptateur / rang / domaine, et le dernier cycle publié.

Deux choix qui ne sont pas cosmétiques :

- LES DEUX IDENTITÉS DU BUDGET sont dans le rapport, avec leur verdict. Elles valent souvent
  plus que la description du symptôme : portées fausses, elles désignent le moteur ; portées
  vraies, elles désignent l'écran. Sans elles, le premier échange consiste à les demander.

- Aucun secret n'y entre : ni PIN installateur même haché, ni jeton, ni mot de passe. Un
  rapport voyage — courriel, capture, fil de discussion — et ce qui y entre en sort. Les
  UUID de Things restent : ce sont eux qui permettent de recouper avec le journal de la box,
  et ils n'ont aucune valeur hors de l'installation.

Les absences gardent leur sens, comme partout ailleurs : « aucune télémétrie reçue » n'est
pas un arbitrage à zéro, « budget ABSENT » n'est pas un budget de zéro watt, et un
financement omis se dit omis.

Pas d'envoi automatique : il n'existe aucun service de collecte, et un bouton « Envoyer »
sans destinataire donnerait le sentiment que quelqu'un l'a reçu. Le rapport se copie, ce qui
est vérifiable.

package_info_plus est ajouté plutôt qu'une constante de version recopiée à la main : une
constante dérive de pubspec.yaml en silence, et un rapport qui annonce la mauvaise version
envoie chercher un défaut dans un code qui n'est pas celui-là.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
2026-08-27 14:54:27 +02:00
Patrick Schurig
f7fd91edde fix(recharge): le SOC véhicule inventé disparaît — la grandeur n'existe pas
_SocProgress affichait « 62 % » et « Aujourd'hui à 07:30 » en dur, sur un écran client.
Laissé de côté au lot précédent comme préexistant ; ça ne tient plus, une promesse fausse
ne se périme pas.

Le correctif attendu — brancher la vraie valeur — n'existe pas. Vérifié sur .75 : sur les
58 classes de la box, aucune classe portant l'interface evcharger ne déclare d'état de
charge, aucune ne porte l'interface car, et les deux bornes du banc publient pluggedIn,
charging, maxChargingCurrent, phaseCount, sessionEnergy — jamais un pourcentage.

Le seul batteryLevel de l'installation est celui de la batterie de la MAISON (Fronius
Storage, 50 %, rendu visible par le correctif energystorage du plugin SunSpec). C'est une
autre grandeur : l'afficher en face d'une cible de recharge remplacerait un chiffre faux
par un chiffre faux et crédible. Retiré, avec la barre de progression — une jauge sans
grandeur mesurée est un dessin — et la carte dit maintenant pourquoi l'avancement n'est pas
affichable. Reste ce que l'app SAIT : la cible qu'elle vient elle-même d'écrire.

Le brief 3g-2 est rapatrié depuis le dépôt plugin (la forge étant à terre, il n'avait pas
pu voyager) et CLAUDE.md est recalé dessus. À noter, la description trompeuse de
SetChargingInfo signalée hier est corrigée sur la box : elle énumère désormais tous les
défauts et conclut « send it back complete ».

Le prompt 3g-2 déposé à la racine est commité tel quel, il était déjà indexé.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
2026-08-27 14:33:25 +02:00
Patrick Schurig
4b42b732dc docs: le contrat de recharge et les deux pièges de télémétrie, plus le retour au moteur
CLAUDE.md portait un exemple de SetChargingInfo qui invitait à l'écriture partielle. Il
porte maintenant la règle inverse, avec la preuve machine, et les deux pièges de lecture de
GetLoadTelemetry : ne pas sommer allocatedW sans filtrer sur funding, et ne pas lire
l'allocation d'une borne comme son état.

Retour au moteur dans BRIEF_plugin_depuis_maquettes.md. Un point actionnable chez eux : la
description trompeuse vit dans nymeaenergyjsonhandler.cpp:114 et part dans Introspect chez
tout client. Elle ne se contente pas de ne pas prévenir, elle autorise explicitement
l'erreur — le prochain client la croira. Une ligne à corriger, ou une écriture à rendre
réellement partielle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
2026-08-27 07:39:12 +02:00
Patrick Schurig
054cbfab3c docs: maquettes de la refonte « Charges pilotables » + CLAUDE.md recalé sur la box
Trois maquettes, neuf écrans, après le passage sur appareil qui a montré que
/energy/setup décrit autre chose que l'installation.

- charges_pilotables_mockup.html : la liste à deux niveaux, avec le bandeau
  d'arbitrage hérité de /energy/loads (qui disparaît) et la télémétrie sur la carte
  de chaque charge.
- configurer_mecanismes_mockup.html : les six « Configurer » — routeur de relais,
  modulable, SG-Ready, borne, zones HVAC, sélecteur d'appareils.
- raccordement_reseau_mockup.html : compteur réseau et protection de surcharge.

Décisions de conception que les images ne portent pas seules : les paliers
n'existent que chez le routeur de relais et y sont DÉDUITS des combinaisons ; le
mode fixed d'etmvariableload (legacy) n'est plus proposé mais reste lisible, parce
qu'ouvrir un écran ne doit pas convertir une config déployée ; SG1/SG2 sont une
convention d'app retrouvée par l'encodage standard, la box ne stocke qu'une liste
de relais par état ; le compteur par charge est dessiné inactif avec sa raison ; la
borne règle un mode par DÉFAUT, pas une commande.

CLAUDE.md corrigé contre JSONRPC.Introspect (plugin 1.15.2+etm15) :

- Energy.SetChargingMode n'existe pas — le « bug critique » décrivait un appel
  impossible. Energy.* n'a que cinq méthodes.
- Le namespace EnergyPlugin.* n'existe pas : c'est NymeaEnergy.*, 20 méthodes et
  14 notifications, toutes listées.
- ChargingInfo : chargingMode / targetPercentage / endDateTime (Uint), et non
  mode / targetSoc / endTime. minCurrent n'existe nulle part.
- phasePowerLimit est en AMPÈRES par phase, et 0 coupe toute la recharge
  intelligente.
- AirConditioning expose 8 méthodes, pas 3.

Toutes les méthodes citées ont été revérifiées une par une : aucune introuvable.
Règle ajoutée : contrôler ce fichier contre Introspect avant chaque lot — trois
fois aujourd'hui il a envoyé un agent dans le vide.

Et docs/BRIEF_plugin_depuis_maquettes.md : les six points que les maquettes
remontent vers le moteur, dont un seul blocage dur (pas de champ de courant
minimum pour le mode Min+PV).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
2026-08-25 21:30:32 +02:00
Patrick Schurig
bb77039c82 docs: RECAP session 2026-08-25 + accès ssh banc .75
Point d'arrêt avant redémarrage machine. Consigne l'état du lot B app, le
bloqueur (lot plugin non déployé sur .75), le partage vérifié/non-vérifié,
et la question ouverte sur la fusion /energy/setup ↔ /energy/loads.

Note l'accès ssh au banc désormais documenté dans CLAUDE.md : il lève la
limite qui rendait le critère 7-1bis (non-reconstruction des adaptateurs)
invérifiable faute de journal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 14:31:58 +02:00
Patrick Schurig ETM-Schurig
7d71bea527 chore: mise à jour CLAUDE.md — brief refonte UI agent
Remplace le CLAUDE.md générique par le brief complet de la session :
APIs JSON-RPC consommées, état des écrans, persistance, feature gating,
thème EtmTokens, règles de modification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 22:10:50 +02:00
1278da2a04 chore: add CLAUDE.md agent context 2026-04-04 17:35:57 +02:00
etm
d5dc0c7ca5 Initial commit : Flutter app nymea energy monitor
- NymeaService : auth complète (Hello → Authenticate → SetNotificationStatus)
- Token top-level dans chaque requête JSON-RPC (fix critique GetThings)
- Persistance token via shared_preferences par hôte
- Dashboard : champs utilisateur/mot de passe dans le dialog de connexion
- ThingDetailScreen : renommer, réglages (settingsTypes) et supprimer
- NymeaThingClass : champ settingsTypes parsé depuis l'API
- NymeaThing : copyWith(name) + settingValue()
- Fix overflow _StateChip dans ThingsScreen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:57:46 +01:00