Patrick, 2026-10-16 : le titre disait « oct. 2026 » au-dessus d'une vue
des 30 derniers jours (17/9 → 16/10). L'onglet Mois prend désormais le
mois civil du jour choisi, du 1er au dernier jour — barres, tuiles du
bilan et titre disent la même chose. Les flèches avancent d'un MOIS (et
non de 30 jours : le 31 janvier menait au 2 mars) ; revenir sur le mois
en cours reprend le suivi d'aujourd'hui. Vu sur le téléphone (oct. 2026,
sept. 2026).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vu sur le téléphone le 2026-10-16 (demandes de Patrick).
- Notes retirées sous les graphes : absence de batterie, geste de
glissement, séries par consommateur, et les deux notes sur la prévision
(vue Heures seulement, plage non couverte). Les notes sous les tuiles du
bilan restent.
- Graphes en barres : fl_chart appelle l'étiquette de CHAQUE barre en
ignorant `interval` — 24 heures ou 30 dates imprimées les unes sur les
autres. Une étiquette tous les _xIntervalPeriodes.
- La paire qui commence avant la période (point d'appui demandé un pas
plus tôt) n'est plus dessinée comme une barre : « 23:00 » en tête de la
vue Jour, « 9/10 » en tête d'une semaine du 10 au 16.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Demande de Patrick du 2026-10-16 : les six tuiles (production,
consommation, réseau, taux) disaient toujours la journée en cours, quel
que soit l'onglet.
- Heures / Jour : le jour choisi (aujourd'hui : la journée de la BOX,
periodStartedAt, comme avant). Semaine / Mois / Année : les 7, 30 ou
365 jours qui finissent le jour choisi — la plage des barres.
- NymeaService.fetchBilan : le calcul du bilan journalier généralisé à une
période (différence des compteurs, mêmes refus). Borne de départ = le
dernier échantillon AU PLUS TARD au début : sur un pas d'un jour, le
premier échantillon postérieur serait la fin du premier jour.
- « Bilan · 10 oct. → 16 oct. 2026 » au-dessus des tuiles.
- Précision selon la grandeur (« 286.22 k… » était tronqué) : MWh au-delà
de 1 000 kWh. Note « période partielle » datée au lieu de « journée
partielle avant 01:00 » sous un bilan d'un an.
Vu sur le téléphone (Heures, Semaine, Année). Sur l'année, le banc montre
63 MWh produits : saut des compteurs cumulés le 2026-08-03 (0 → 62 008
kWh), pas un défaut du calcul.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vu sur le téléphone le 2026-10-16 (première vérification à l'écran).
- « En ce moment » : une ligne complète seulement pour une charge qui AGIT
(puissance portée sous un motif qui n'est pas une attente, puissance
mesurée, dérogation, défaut, manque retenu) ; les autres sur une ligne
discrète « En attente : … » (Patrick). La PAC SG-Ready qui porte 1 200 W
en état normal maintenu sous BELOW_MIN_POWER attend.
- Motif affiché = celui du niveau qui porte la plus grosse part (règle R7
d'etm70) : en etm69 la racine disait « rien n'est commandé » pendant que
l'éco achetait 83 W. LoadDecision lit désormais `level`.
- Écran Énergie : titre de l'écran et titres des cartes de graphe
invisibles en sombre (sans() = bleu marine par défaut) → inkOf ;
étiquettes des bords du graphe « Qui consomme » qui se chevauchaient
(« 09:4510:00 ») retirées ; axe de la prévision qui affichait
« -0.0kW » pour quelques watts nocturnes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consigne app §G (décision Patrick du 2026-10-16) et motifs etm70 (§H).
G — la carte « Héos pilote en ce moment » affichait des décisions
inventées (HeosDecision.demo : voiture 3,6 kW, batterie en charge sur un
banc sans batterie). Remplacée par EnCeMomentCard : une ligne par charge
de GetLoadTelemetry — libellé de la configuration, decision.code + params
rendus par decisionText, puissance MESURÉE seulement (jamais l'allocation
à sa place). Titre en clé l10n. Rien sur « optimiseur actif » tant que
l'objet de capacités n'existe pas.
Marque retirée des identifiants (HeosDecisionCard, HeosDecision,
HeosAction, _HeosChip, _HeosBadgeSmall, heosActive → pilotageActif…), des
fichiers (heos_decision_card.dart, dashboard_models.dart supprimés), des
textes affichés (hub du flux, écran A/C) et des commentaires :
grep -ri héos/heos vide sur lib/, test/, assets/ et les .arb.
H — contre INTERFACE.md local de la branche moteur, etm70 pas encore sur
le banc :
- motifs éco publiés à la racine (R7, part majoritaire) rendus en clair ;
ventilation des niveaux affichée (« 83 W éco + 0 W confort », vu sur
la trame réelle etm69) ;
- ECO_FLOOR_MISSED retenu par charge (publié un seul cycle), y compris
pour une charge sortie de loads[] ; « manque non chiffrable » quand
missingWh est absent, jamais 0 Wh ;
- PRICE_SLOT et OPTIMIZER_DECISION en FR / DE / EN (app_en.arb et
app_de.arb créés pour ces seules clés ; le reste retombe sur le
français, listé dans l10n_untranslated.json). Code inconnu : repli
générique avec le code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consigne app §F, vérifiée sur le banc le 2026-10-15.
- Lecture GetTariff à l'ouverture : aucun tarif / fixe / HC-HP (deux
zones, la moins chère sur UNE plage) / lecture seule pour tout le reste
(HC en deux fenêtres, > 2 zones, prix égaux) — jamais réécrit.
- Écriture SetTariff en c€/kWh (€ × 100, 4 décimales) ; HC/HP : la plage
HP est le complément exact de la plage HC ; revente facultative. Relue
après succès (aucune notification).
- Refus TARIFF_REFUSED rendu en français depuis faultParams (cause, zone
nommée par le libellé envoyé, minute en cause).
- Heures affichées comme celles de la box (timeZone).
- Retirés : prix par défaut codés en dur, stockage local, fournisseurs
fictifs, prix « live » inventés. Menu « Tarifs & providers » → « Tarif ».
- Test : ce que l'app enverrait est identique, champ pour champ, au tarif
réel du banc (aucune écriture faite sur la box).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Variante E de la maquette, retenue par Patrick le 2026-10-15 (« deux
colonnes à la Victron ») ; remplace la carte « Production du jour ».
- Par heure, deux colonnes : à gauche la consommation, Maison (bleu) puis
chaque consommateur déclaré avec compteur (couleur de son domaine, même
règle que l'écran Énergie) ; à droite le solaire, autoconsommé (doré)
puis injecté (pâle), la prévision en fond pour comparer. Heures à venir :
la prévision seule, en contour — aucun plan n'est publié par la box.
- Échantillon horaire horodaté T = heure qui commence à T − 1 h, pour le
bilan comme pour les Things.
- Météo de maintenant lue sur le Thing site openmeteo, et seulement si
updateTime a été écrit : les défauts du plugin (clear-day, 0 °C)
afficheraient sinon un soleil à 0 °C. Les icônes horaires attendent
openmeteo etm16 (consignes/CONSIGNE-openmeteo-meteo-horaire.md).
- Charges sans compteur déclaré nommées sous la carte (« comptée dans
Maison »), jamais devinées.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Demande de Patrick du 2026-10-15, alignée sur la lecture Victron :
- autoconsommé : jaune doré (était orange) — tout le solaire est jaune,
l'exporté en jaune pâle par-dessus (nouveau jeton exportSolaire) ;
- SOC de la batterie : vert pointillé (était l'indigo des bornes) ;
- consommation / soutiré : bleu, inchangé.
Le jeton garde son nom (selfUse) : c'est son rôle, pas sa teinte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
« Prix unique appliqué à toutes les décisions du scheduler » était faux :
TariffProvider.load() ne lit ni n'écrit rien sur la box, et le moteur ne
décide sur aucun prix. Remplacé par « Saisie locale, pas encore transmise
à la box. » Rien d'autre n'est branché, en attendant le RPC tarif (T4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consigne app etm66, section D (décision Patrick du 2026-10-14).
- Bande prévu × (1 ± s_J), s_J = dispersion du jour J lue sur les Things
plan openmeteo (forecastWeatherSpread{Today,Tomorrow}Percent, plus forte
des plans). J est le jour civil de PARIS : openmeteo découpe ses jours en
Europe/Paris, écrit en dur dans sa requête.
- Bas arrêté à 0, haut au plafond AC sinon à la crête cumulée des plans
(kwp). Créneau absent = trou, bande comprise. 0 % sans modèle d'ensemble
= état jamais écrit = pas de bande.
- Tracée atténuée, « non datée » : openmeteo etm15 ne date pas la valeur
et la garde quand l'API d'ensemble échoue.
- p10W / p90W publiés par la box remplacent la fourchette (code prêt,
inactif). La bande n'est jamais renvoyée à la box.
- Accueil : fourchette du total du jour quand chaque créneau en a une.
- Détails installateur retirés sous le graphe (demande de Patrick).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Demandes de Patrick du 2026-10-14.
- Écran Énergie, vue Heures : la prévision partage la fenêtre de 12 h et
le glissement des graphes du dessus. Les instants sont convertis en
index fractionnaire (AxeEchantillons : interpolé entre deux échantillons,
extrapolé au pas au-delà) ; seule la borne droite de la fenêtre peut
dépasser la dernière mesure, jusqu'à la fin de la prévision. Prévision
semi-transparente, mesuré en plein par-dessus.
- Accueil : carte « Production du jour » (0 h → 24 h, barres horaires),
sous les décisions. Prévision derrière, mesuré devant. Un échantillon
SampleRate1Hour horodaté T est la moyenne de (T − 1 h, T]
(energylogger.cpp) : il va dans l'heure qui commence à T − 1 h. Totaux
affichés seulement quand ils sont complets.
- PvForecastProvider : une lecture et un minuteur (12 min) partagés par
les deux écrans, remis à zéro au changement d'installation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consigne app etm66/etm68, vérifiée contre le banc hems (1.15.2+etm68) le 2026-10-14.
- PvForecastCard remplace le placeholder « Météo & prévision » : lit
NymeaEnergy.GetForecastSnapshot à l'ouverture puis toutes les 12 min
(aucune notification n'existe). Pas des créneaux DÉDUIT, créneau absent
= trou (jamais zéro), fault ⇒ pas de courbe avec son motif, libellé
« plafond optimiste », corrections/plafond AC en mode installateur.
- MISSING_FORECAST_SITE : les candidats de GetForecastSite sont proposés,
la désignation (SetForecastSite) se confirme ; acLimitW omis = inchangé.
- Production mesurée superposée depuis GetPowerBalanceLogs, signe inversé
(pas .abs()), aucun cumul total*.
- Marché spot masqué dans Tarifs (SPOT_MARKET_NOT_IN_BUILD) : mode Spot
Market, fournisseurs Tibber/Octopus/ENTSO-E et la carte de prix codés en dur.
- Bornes VE envoyées en {uuid} minuscules jusqu'à etm69 (SetChargingInfo,
SetLoadConfig, Set/ClearLoadDerogation) via idBorneCanonique().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le banc `hems` est renuméroté (192.168.1.75 → 10.42.60.21). Portés : les 11
sondes `tools/rpc/*` et leur README, les 3 tests on-device, le `hint` de
l'écran Installations, les procédures de CLAUDE.md et des deux documents de
cadrage.
Ce qui n'est PAS touché, et c'est délibéré : les relevés datés (RELEVE_LOTC,
RECAP, Prompt 3g2, maquettes) et les fixtures `hems75_*.json`. Un relevé daté
dit ce qui a été observé ce jour-là ; le réécrire échange une trace contre une
cohérence de surface. CLAUDE.md porte la note qui fait le lien : `.75` et
`hems75` désignent la même machine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DEUX DEMANDES DE PATRICK.
1. Une barre de navigation sous les onglets, à la place de la puce de date :
‹ 3 sept. → 9 sept. › Aujourd'hui
Le libellé suit la granularité de l'onglet — « 9 sept. 2026 », « 3 → 9 sept. »,
« sept. 2026 », « 2026 » — parce que dire « jour » en montrant une semaine est
exactement l'erreur que les onglets faisaient avant d'être corrigés.
Le sélecteur existait déjà dans la barre du haut, mais il permettait de sauter
à un jour, pas de reculer d'une semaine puis d'une autre. Or lire une
consommation, c'est presque toujours la comparer à la période précédente : ce
geste-là doit coûter un appui, pas l'ouverture d'un calendrier et le calcul
mental de la date d'avant.
La flèche « suivant » est GRISÉE, pas masquée : une commande qui disparaît
laisse chercher où elle est passée, une commande grisée dit « il n'y a rien
après ». Et elle est bornée au présent — sans ça on part dans un futur où la
box n'a rien, avec un graphe vide qui ne dit pas qu'il l'est PARCE QU'on est
allé trop loin.
2. Le sous-titre « le bloc orange est partagé : ce qui a été produit ET gardé »
est supprimé.
Et une variable morte retirée avec sa raison : le soutiré n'est pas relu de
`totalAcquisition` mais dérivé de `consommation − autoconsommé`. Le relire
ferait une pile dont les segments ne rejoignent pas leur sommet dès qu'un
compteur dérive — un trou dans la barre que personne ne saurait lire.
L'identité des compteurs se vérifie ailleurs, sur le bilan du jour, où elle a
une phrase pour se dire.
358 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
LA DATE PASSE ENTRE LA LÉGENDE ET LE TRACÉ (demande de Patrick). Sa place n'est
pas cosmétique : elle QUALIFIE ce qu'on regarde, donc elle doit se lire avant le
graphe, pas se trouver après lui. Sous les deux cartes, elle arrivait une fois
la lecture faite — et sur trois jours, une courbe lue sans sa date est une
courbe lue de travers.
Posée sur les DEUX cartes : elles partagent le cadrage, mais si on défile
jusqu'à ne voir que la seconde, elle doit se suffire. Une carte qui dépend d'une
autre pour être comprise ne tient pas dès qu'on la regarde seule.
🔴 ET L'ÉCHELLE SUIT DÉSORMAIS LA FENÊTRE, PAS TOUT LE JEU CHARGÉ.
Vu à l'écran : axe à 11,9 kW pour une fenêtre qui ne dépasse pas 2,5 — les
courbes rampaient au ras de l'abscisse, et la production était purement
invisible. Une pointe d'avant-hier écrasait le détail d'aujourd'hui.
C'est le passage à 72 h qui l'a créé, et c'est lui qui l'a rendu visible : sur
24 h, fenêtre et jeu de données avaient presque la même amplitude, donc le
défaut ne pouvait pas se voir. Élargir le glissement sans rééchelonner faisait
une loupe qui déplace le cadre sans grossir — exactement ce qu'il devait éviter.
Corrigé sur les deux graphes : cadrer l'un sur la fenêtre et l'autre sur tout
les rendrait incomparables.
Captures : docs/captures/2026-09-09_energie-plage-sous-legende.png
docs/captures/2026-09-09_energie-echelle-fenetre.png
358 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Demande de Patrick : élargir le glissement. 72 h chargées au quart d'heure, la
fenêtre en montre 12 — on remonte donc sur trois jours sans changer d'onglet.
TROIS CHOSES QUE CE CHANGEMENT RENDAIT NÉCESSAIRES.
La taille de fenêtre était « la moitié du jeu de données ». Avec 24 h ça donnait
12 h ; avec 72 h ça en aurait donné 36, et la fenêtre aurait grandi avec les
données au lieu de rester une fenêtre. Elle est fixée en DURÉE : la densité de
lecture ne dépend plus de la profondeur chargée.
La fenêtre s'ouvre sur le plus RÉCENT. Calée au début, elle aurait montré
avant-hier soir à l'ouverture — la seule vue dont personne n'a besoin en
premier.
Et la plage visible porte sa DATE : « 8/9 21:00 → 9/9 09:00 ». Sur 24 h,
« 09:00 » désignait sans ambiguïté ce matin ; sur trois jours, c'est l'un de
trois matins, et l'axe n'affiche que des heures. Sans cette ligne, l'écran
répondait à côté de la question « c'était quand ? ».
Et les étiquettes des deux BORDS de l'axe sont retirées : fl_chart en pose une à
`min` et à `max` en plus de celles de l'intervalle, et elles se superposaient à
leur voisine — « 20:4500 » à gauche, « 08:1509:00 » à droite. J'avais corrigé le
bord gauche sans penser que le droit avait le même défaut : la recherche
auto-confirmante, appliquée à un axe. On ne perd rien, la ligne de plage donne
les deux bornes exactes avec leur jour.
Vérifié à l'écran : fenêtre 21:15 → 09:00 à l'ouverture, ligne de plage et
indice de geste présents.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
TROIS DEMANDES DE PATRICK.
1. Le soutiré passe du rouge au BLEU. Avec l'orange partagé sur les deux barres,
le rouge rendait l'image trop chaude — et le bleu est déjà la couleur de la
maison partout ailleurs. Il ne reste d'orange que le bloc partagé, celui qui
doit se voir.
2. Second graphe en vue barres : « Où est passée la consommation » — une barre
empilée par période, une couleur par charge déclarée, plus le reste du foyer
qui ferme la barre à la consommation totale.
L'énergie vient du compteur cumulé de chaque appareil (`totalConsumption`),
jamais d'une intégration de la puissance — intégrer supposerait la puissance
constante entre deux échantillons. Vérifié à la sonde avant d'écrire : ECS
80,8 kWh et PAC 43,1 kWh sur la fenêtre, avec des deltas journaliers réels.
Une charge dont le compteur n'a pas bougé donne 0 kWh, qui est une MESURE.
Une charge dont le cumul manque à une borne n'a PAS de part. Les deux se
ressemblent à l'écran et ne disent pas la même chose. Et un compteur qui
recule est une remise à zéro, pas une consommation négative : la part est
refusée plutôt que rendue absurde.
🔴 3. LE « 6/9 » EN DOUBLE N'ÉTAIT PAS UN DÉFAUT DE L'APP. C'est la trace de
notre propre bascule de fuseau du 2026-09-06 : la frontière de journée de la
box a glissé d'une heure, et l'historique porte deux échantillons pour le
06/09 — 00:00 et 01:00. Le graphe affichait fidèlement une « journée » d'une
heure à côté de journées entières.
La couture est PERMANENTE : elle est dans les données passées, pas dans une
requête. Tout écran qui compare des périodes doit grouper par période civile,
jamais par indices consécutifs. Aucune énergie n'est perdue — la tranche
d'une heure est absorbée dans sa journée.
Et une étiquette d'axe illisible : fl_chart en pose une à `min` en plus de
celles de l'intervalle, et les deux se superposaient — « 20:4500 ». Deux heures
imprimées l'une sur l'autre ne se lisent ni l'une ni l'autre.
358 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Les onglets portaient chacun le nom d'un cran EN DESSOUS de leur plage : « Jour »
montrait 7 jours, « Semaine » 28, « Mois » 365. Quatre étiquettes, quatre
mensonges — et la journée que le premier promettait n'était nulle part.
L'onglet « Année » manquait, puisque c'est « Mois » qui la montrait.
Heures 24 h au ¼ h courbes, axe glissable
Jour 00:00 -> 24:00 barres, au pas horaire
Semaine 7 jours barres, au jour
Mois 30 jours barres, au jour
Année 365 jours barres, au mois
LES BARRES. Deux barres par période, chacune empilée, et le segment
« autoconsommé » est LE MÊME des deux côtés, à la même couleur :
Production = autoconsommé + exporté
Consommation = autoconsommé + soutiré
Deux barres simples diraient laquelle est la plus haute ; celles-ci disent où
est passée la différence — la question qu'on se pose devant un bilan.
Pas de diagramme divergent, et la raison est propre à ce dépôt : la box publie
déjà `production` avec un signe négatif, défaut qui nous a coûté deux écrans et
trois jours. Dessiner la production sous l'axe rendrait une capture correcte
indiscernable de ce bug.
L'énergie vient des CUMULS, jamais des puissances : chaque barre est une
différence de compteurs. Une période dont une borne ne publie pas ses cumuls n'a
pas de barre — pas une barre à zéro. Et la fenêtre demandée déborde d'un
échantillon avant le début, sans quoi la première barre n'a pas de prédécesseur.
Quatre défauts trouvés à l'écran, tous invisibles au test :
- `barGroups` était OUBLIÉ. Les groupes étaient calculés, `maxY` en découlait,
donc l'axe montait à la bonne hauteur — et aucune barre n'était dessinée. Tout
ce qui entoure les données avait l'air correct ;
- « 80.0kWh » ne tenait pas dans 46 px et se coupait en « 80.0kW / h ». Une
unité coupée en deux lignes se lit comme une autre unité ;
- une plage de 7 jours partant de 08:12 coupe deux journées en deux : deux
barres portaient « 6/9 » et la semaine en comptait huit. Toutes les vues en
barres se calent maintenant sur minuit ;
- les notes par consommateur s'affichaient sous les barres, où aucun
consommateur n'est tracé.
358 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Demande de Patrick. Fait par FENÊTRE glissante (minX/maxX) plutôt qu'en faisant
défiler le graphe entier : l'échelle en kW reste visible pendant le geste, alors
qu'un défilement horizontal l'emporterait hors du champ au moment où on en a
besoin.
⚠️ On borne minX/maxX, on ne DÉCOUPE pas les données : découper renumérotrait
les index, et l'axe du bas — qui lit `_data[x.round()]` — désignerait alors les
mauvaises heures. Les séries restent entières, la fenêtre ne fait que cadrer.
96 points au quart d'heure sur 24 h : tout montrer écrase le détail, et il n'y a
alors rien à faire glisser. La fenêtre en montre la moitié.
Trois détails qui vont avec :
- le geste convertit des pixels en points d'échantillon, donc glisser d'une
largeur d'écran déplace d'une fenêtre entière quelle que soit la densité ;
- la fenêtre est bornée aux données — glisser dans le vide ferait croire qu'il
n'y a rien à cet endroit, alors qu'il n'y a pas d'endroit ;
- l'espacement des étiquettes se calcule sur ce qui est VISIBLE : le calculer
sur le jeu entier en placerait trois, mal réparties.
Le détecteur de geste n'est posé que sur les vues glissables : envelopper un
graphe qui ne glisse pas volerait le défilement vertical de la page pour rien.
Et l'indice ne sort que là où le geste existe.
Les deux graphes partagent le même cadrage — comparer deux fenêtres différentes
n'aurait aucun sens.
358 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Décision de Patrick, réaffirmée : « tu gardes tout, seulement le bouton boost on
supprime ».
Je l'avais écrit et posé quelques heures plus tôt, en lisant « le bouton de
boost : ouvre-le quand tu veux » dans un message du moteur comme un feu vert —
alors que la décision produit était déjà prise en sens inverse. Un avis
technique sur la faisabilité ne révise pas un arbitrage de produit ; il fallait
demander, pas inférer.
Retiré : l'usage de `EvBoostButton` sur l'écran de la borne, son import, et le
helper `_entreeTelemetrie` devenu orphelin.
Gardé, sur instruction explicite : le widget, `setLoadDerogation` /
`clearLoadDerogation`, le modèle `Derogation` / `DerogationEnded` et leurs 19
tests. Ce n'est pas de la donnée fabriquée comme `DayPlan.demo()` — c'est la
lecture d'un contrat réel, testée, qui ne s'affiche nulle part et ne peut donc
rien afficher de faux. Le jour où le boost revient, il se rebranche en trois
lignes.
Vérifié à l'écran : « Mode de recharge · lu : Min+PV », trois boutons Manuel /
PV / Min+PV, Min+PV allumé parce que c'est ce que la box porte. Rien d'autre.
Capture : docs/captures/2026-09-08_ev-trois-modes.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Vu à la capture du bouton de boost : la note « le mode de recharge se règle
depuis la tuile Borne EV des favoris » est visible trois centimètres au-dessus
des boutons de mode, dans le même défilement. Les boutons ont été déplacés ici
le 2026-08-28 et la phrase ne l'avait pas suivi — elle envoyait chercher ce qui
est sous les yeux.
Capture des trois modes et du bouton d'action :
docs/captures/2026-09-08_ev-modes-et-boost.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Le lot complet côté app, écrit sur le contrat vérifié dans Introspect sur .75 en
1.15.2+etm61 : SetLoadDerogation { loadId, durationS, o:targetWh } /
ClearLoadDerogation { loadId }, et derogationEnded[] { cause, loadId,
o:restoredMode } au niveau de la trame.
CE QUI EST UN BOUTON D'ACTION, PAS UN MODE. Une dérogation se superpose au mode
courant et s'efface ; elle ne le remplace pas. La mettre dans le sélecteur
ferait croire qu'on en sort en choisissant autre chose.
UNE DURÉE, TOUJOURS. Bornée [60, 21600] côté moteur. L'écran n'offre AUCUNE
option « jusqu'à annulation » — pas parce que l'appel échouerait, mais parce que
la promesse serait mauvaise : une dérogation qui ne sait pas dire quand elle
finit ne devrait pas exister (LM-1500). Le filet reste armé même avec une cible,
parce qu'une cible repose sur un instrument et qu'un instrument peut se taire.
L'ÉTAT NE SE LIT PAS DANS LE MODE. Si la borne était en manuel, le moteur force
Eco le temps de la dérogation : GetChargingInfos montrerait alors un mode que
personne n'a choisi. La vérité est sur la charge —
LoadTelemetryEntry.enDerogation.
Trois refus d'inventer, chacun figé par un test :
- entrée de télémétrie absente -> le bouton se TAIT. « Pas d'entrée » n'est pas
« pas de boost », c'est « on ne sait pas » ;
- échéance dépassée alors que la box annonce encore la dérogation -> on la NOMME
au lieu de rabattre à « 00:00 » : c'est une contradiction entre deux champs de
la même trame, et le zéro l'effacerait ;
- cause de fin inconnue -> aucun libellé. Une box plus récente peut en publier
une septième ; lui coller la phrase d'une autre serait pire.
Et `EnergyErrorInvalidParameter` sur l'arrêt ne crie pas au refus : la
dérogation a pu finir seule pendant l'appui, et une course que l'utilisateur ne
peut pas éviter n'est pas une erreur à lui montrer.
⚠️ boundBy a DEUX catalogues : drawCap|loadCeiling sur la dérogation, mais
« minimum » sur ECO_FLOOR_GRID — relevé sur la V2C aujourd'hui. Même nom, sens
différent : aucune traduction partagée.
⚠️ Écrit d'après le contrat, PAS observé : DEROGATION_ACTIVE, endsAt et boundBy
ne sont pas dans Introspect (les params d'une décision sont une carte libre), et
aucune dérogation n'a tourné pendant une sonde. Tous les champs restent
nullables. Noté aussi qu'Introspect SOUS-DÉCLARE — il annonce draw{committedW}
seul alors que la box publie authorisedW/binding/bindingPhase/perPhaseBound —
donc son silence ne prouve rien.
La feuille de choix défile : 47 px de débordement trouvés par le test sur une
surface courte, donc réels sur un téléphone à police agrandie.
358 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
DayPlan.demo() fabriquait vingt-quatre heures de flux : une gaussienne solaire
et des coefficients écrits en dur — solToEcs = excédent × 0,35, solToVe × 0,3,
solToBatt × 0,15, battToHouse = déficit × 0,55 — sous le titre « Plan de la
journée · aujourd'hui », sans la moindre mention. Y compris une batterie
alimentant la maison à partir de 17 h, sur une installation qui n'a pas de
batterie.
Cinquième après les deux MPPT, les 38 % de PAC, les 82 % de part solaire et le
SOC à 62 %. Même réponse à chaque fois : il n'y a rien derrière.
Un plan suppose de savoir ce qui VA arriver — il demande une prévision PV et une
source tarifaire, dont aucune n'existe. La carte reviendra avec elles.
Le code de fabrication part AVEC la carte : DayPlan, HourlyFlow, WeatherPoint,
WeatherKind et le painter sont supprimés, pas mis de côté. Un générateur de
données plausibles laissé dans le dépôt se rebranche — c'est exactement ce qui
s'est passé quatre fois déjà.
339 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Décision de Patrick : trois modes suffisent — Manuel, PV, Min+PV. Le boost n'est
pas un quatrième mode, c'est la dérogation (+etm61).
LE FAIT. Le bouton écrivait `ChargingModeNormal`, qui sort la borne de
l'arbitrage et lui restitue ses valeurs manuelles stockées —
manualChargingEnabled, manualMaxChargingCurrent, manualDesiredPhaseCount. Sur
.75 elles valaient false / 6 A / 3 phases : mesuré le 2026-09-08 à 11:09:51,
l'appui a ARRÊTÉ la charge et armé un verrou de 300 s. Pas une régression —
c'est ce que Normal a toujours fait. C'est le nom qui mentait.
Renommé jusque dans le code (`ChargingMode.boost` -> `ChargingMode.manuel`), pas
seulement sur l'étiquette : un identifiant faux reste un piège pour le prochain
lecteur, et c'est précisément ce qui a duré des mois ici. L'ordre met Manuel en
tête, parce que ChargingModeNormal est aussi le mode d'office d'une borne
détectée.
ET LE RENOMMAGE NE SUFFISAIT PAS. « Manuel » dit QUI commande, pas ce que la
borne va faire : tant que l'app n'offre pas de régler ces valeurs stockées, le
mode veut dire « ce qui traînait la dernière fois ». Passer en manuel demande
donc confirmation, et la confirmation dit la conséquence — la charge peut
s'arrêter — AVANT, pas après.
⚠️ La confirmation ne se déclenche pas sur un ré-appui du mode déjà en place :
une confirmation qu'on apprend à cliquer sans lire ne protège plus de rien. Un
test le fige.
Vérifié sur .75 (1.15.2+etm61) : SetLoadDerogation et ClearLoadDerogation sont
dans Introspect, avec derogationEnded / durationS / restoredMode. Le contrat de
la dérogation est écrit dans CLAUDE.md — durée obligatoire bornée [60, 21600],
état lu dans decision.code == "DEROGATION_ACTIVE" et jamais dans le mode, fin au
niveau de la trame avec six causes. Aucun bouton de boost n'est écrit : c'est le
lot suivant.
Et une sonde pour le moteur, faite au passage : la V2C est bien restée en
ChargingModeNormal et elle est ABSENTE de loads[] — hors arbitrage. Leur
hypothèse est confirmée sans redémarrage.
339 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
La V2C et la borne simulée, toutes deux en `ev`, recevaient exactement le même
violet : indiscernables dans l'empilement, et la légende répétait la même
pastille. Vu à l'écran dès qu'une seconde borne est apparue.
`EtmTokens.nuance` décline sur le RANG dans le domaine, donc une troisième borne
prend sa teinte sans qu'on déclare quoi que ce soit. Le décalage porte sur la
LUMINOSITÉ, pas sur la teinte : glisser la teinte finirait par heurter le
domaine voisin, l'éclaircir garde la famille lisible.
⚠️ La série est bornée à quatre, et c'est explicite plutôt que subi : au-delà,
l'écart ne se voit plus, et prétendre distinguer huit nuances d'un même violet
serait une promesse que l'œil ne tient pas. Le rang 3 et le rang 9 rendent la
même couleur — le signe qu'il faut alors autre chose que la couleur pour
identifier, et c'est un choix d'écran, pas un réglage de palette. Un test le
fige.
La couleur est calculée à UN endroit, partagé par le tracé et la légende : deux
sources de couleur, c'est une légende qui finit par désigner la mauvaise bande.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
CARTES. Elles lisaient `energyData.dayProductionWh`, qui porte en réalité le
compteur CUMULÉ depuis l'origine : « Production 62946.74… » au-dessus d'un
graphe de 24 h. Et « Gains 8789.84 € » n'avait aucune source — un prix du kWh
que personne n'a saisi. Elles lisent maintenant DailyBalance, comme le tableau
de bord, et chaque taux porte son dénominateur (« 22 % sur 1,1 kWh produits »).
Mise en page reprise de la maquette validée.
V2C. `powersync-plugin-v2c 1.15.2+etm14` lui a ajouté `smartmeterconsumer` :
vérifié à la sonde, elle rend 1 point contre 45 aux autres, son historique
commençant à 10:57. Le filtre d'alignement précédent l'aurait sortie du TRACÉ en
la laissant dans la LÉGENDE — le défaut qu'on venait de corriger sur l'autre
graphe. L'alignement passe donc par instant, et une série ne contribue que là
où elle a des points.
Avant son premier point, sa consommation est comptée dans le reste du foyer :
le total reste juste, seule la RÉPARTITION est incomplète, et c'est ce que dit
la note. Trois états, trois phrases — « pas d'historique », « mesurée depuis
HH:MM », « 0 W mesuré » — et aucune ne se dessine comme une autre.
Et le commentaire de fetchThingPowerLogs est corrigé deux fois : il était
périmé, et il nommait le mauvais portier. `smartmeter` est une interface VIDE
qui ne conditionne rien ; c'est `smartmeterconsumer` qui décide, via
EnergyManagerImpl::watchThing().
🔴 SIMULATION. L'écran faisait `if (!connected) startSimulation()` au montage.
Monté avant que la WebSocket soit établie — le cas normal — il basculait tout le
service en démo et n'en ressortait jamais. Vu à l'écran, box joignable : une
sinusoïde orange et un SOC à 75 % sur une installation SANS batterie. Rien ne
distinguait ces données inventées des vraies : ni couleur, ni mention, ni forme.
La fabrication sans même l'étiquette qui la blanchit.
Le tableau de bord avait retiré la même chose pour la même raison : la démo est
un CHOIX, pris depuis l'écran Installations, jamais un repli silencieux. L'écran
attend maintenant la connexion et se rattrape dès qu'elle arrive.
Captures : docs/captures/2026-09-08_energie-cartes-journalieres.png
docs/captures/2026-09-08_energie-empilement-v2c.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Validé par Patrick. Sa proposition — production en jaune, autoconso en orange,
et des traits pleins — partait d'un bon constat, mais la couleur ne pouvait pas
régler le problème : superposées, autoconsommation et production sont égales au
watt près dès que rien n'est exporté, et deux traits pleins superposés n'en
laissent voir qu'un, quelles que soient leurs teintes. Le défaut est
géométrique, pas chromatique — et le pointillé posé avant n'était qu'un
contournement.
Empilées, le recouvrement devient impossible :
jaune PV -> réseau (exporté) ┐
orange PV -> maison (autoconsommé) ┘ sommet = production totale
La production cesse d'être une courbe : elle EST le haut de la pile. Quand rien
n'est exporté la bande jaune a une hauteur nulle — vrai, et lisible d'un coup
d'œil. L'export devient une SURFACE au lieu d'un écart à mesurer à l'œil entre
deux traits.
Vérifié à l'écran : l'orange porte le fond de la journée, le jaune ne sort que
sur l'épisode d'export de fin d'après-midi.
Deux détails qui vont avec :
- `_aire` ne courbe pas (`isCurved: false`) — une spline peut passer sous zéro
entre deux points, et une bande d'énergie sous l'axe affiche une grandeur qui
n'existe pas ;
- les lignes n'ont plus de remplissage par défaut : un voile à 7 % recouvrait
les bandes et faussait leurs teintes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
« SOC % » figurait en dur dans la légende du premier graphe. Sur une
installation sans batterie — celle du banc — l'écran annonçait donc une série
qu'aucune donnée ne pouvait remplir. Même défaut que partout ailleurs (montrer
ce qu'on n'a pas), pris par la légende au lieu du tracé.
Le titre, le sous-titre et l'entrée de légende suivent maintenant la présence
d'une batterie, et la note d'absence passe SOUS ce graphe-ci : c'est là que le
SOC aurait été tracé, donc c'est là qu'il manque.
`_sansBatterie` exige que les Things soient chargés : tant qu'ils ne le sont
pas, « pas de batterie » et « on ne sait pas encore » sont indiscernables, et
annoncer l'absence trop tôt serait une absence côté APP présentée comme une
absence côté BOX.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Maquette validée par Patrick (couleurs comprises). Deux graphes, et trois
défauts d'affichage trouvés en chemin — dont deux à l'écran seulement.
GRAPHE 1 — les trois « on ne voit pas » avaient trois causes.
production : publiée NÉGATIVE (−103 W). La magnitude est prise AU RENDU, pas
au transport : le champ reste brut (§7.1), et `.abs()` est licite sur une
production, dont le signe ne porte aucune information — contrairement au
réseau, où le signe dit le sens (d'où gridImportW/gridExportW là-bas).
autoconsommation : `production − export` clampé à 0, donc TOUJOURS zéro avec
une production négative. Même racine, corrigée par la même magnitude.
SOC : correctement absent — aucune batterie sur l'installation. Ce qui
manquait, c'était de le DIRE ; c'est maintenant une note sous le graphe.
⚠️ Et un deuxième empêchement, vu seulement à l'écran : le signe réparé, la
production restait invisible SOUS l'autoconsommation. Tant que rien n'est
exporté les deux sont égales au pixel près, et la verte était tracée par
dessus. L'autoconso passe en pointillé — des tirets sur la bande ambre disent
« tout est consommé sur place », leur décrochage dit l'export. Deux courbes
qui coïncident doivent pouvoir se VOIR coïncider.
⚠️ Ce correctif en créait un autre : le SOC était aussi vert pointillé. Sur
une installation AVEC batterie les deux auraient été indiscernables — collision
qu'aucun banc sans batterie ne pouvait montrer. Le SOC passe en indigo.
GRAPHE 2 — qui consomme, empilé.
`Energy.GetThingPowerLogs`, et la liste des appareils vient de GetLoadConfig :
chaque charge déclarée porte son `meterThingId`, son label et son domaine —
vérifié renseigné sur les quatre charges de .75. Rien n'est deviné d'un nom
d'appareil.
Un appareil demandé sans réponse sort dans `sansHistorique` et est NOMMÉ sous
le graphe, jamais tracé à zéro : la V2C ne déclare pas `smartmeter`, donc elle
n'a pas de passé — ce qui n'est pas « elle n'a rien tiré ».
Le RESTE du foyer est licite ici, contrairement au Sankey qui l'avait fait
interdire : chaque part est un compteur dédié et le total est le compteur du
foyer, donc la soustraction ne porte que du mesuré. Et un reste NÉGATIF n'est
pas raboté — c'est la preuve qu'un sous-compteur n'est pas dans le périmètre
du total, et l'écraser effacerait la seule chose qui le dise.
🔴 Le signe des sous-compteurs n'est PAS vérifié : les trois lisent 0 W à tous
les pas, donc aucun échantillon non nul n'existe. Le compteur PV publie en
négatif — rien n'autorise à supposer. La valeur est transportée brute et
`signeIndetermine` le dit à l'écran.
Couleurs par consommateur ajoutées au thème (pac ambre, ecs sarcelle, ev
indigo) : distinctes des couleurs de FLUX, qui disent d'où vient l'énergie, là
où celles-ci disent qui la prend.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
La barre d'action passe en pied d'écran fixe (`bottomNavigationBar`), parce
qu'un correctif juste mais hors du champ reste inaccessible : le geste se fait
en haut de la liste, la barre était un écran entier plus bas. C'est le défaut
d'hier sous une autre forme — cause de disposition au lieu de cause de logique.
`bottomNavigationBar` plutôt qu'une superposition : il RÉDUIT la hauteur du
corps au lieu de le recouvrir, donc rien ne se cache derrière la barre.
⚠️ ET LE DÉPLACEMENT A CASSÉ L'ÉCRAN. Le `Column` d'`ImpactSaveBar` était en
`MainAxisSize.max` — le défaut. Dans un `ListView` la hauteur disponible est
infinie et le Column se contente de ses enfants : le défaut ne pouvait pas se
voir. En pied d'écran la contrainte est lâche mais BORNÉE, le Column a pris
toute la hauteur, le corps a été écrasé à zéro. Écran vide sous une barre
géante, vu en release sur appareil — les 319 tests étaient verts.
Un widget qui ne se contraint pas lui-même hérite du contexte : le déplacer
change ses contraintes, donc sa taille, sans qu'une ligne de son code ait bougé.
Le test ajouté monte la barre dans un `bottomNavigationBar` et mesure sa
hauteur. Éprouvé par son contraire : sans `mainAxisSize: min` il rend 600 sur
600 et échoue. Aucun test précédent ne pouvait mordre — tous montaient la barre
dans une hauteur infinie, où `max` se comporte comme `min`.
Vérifié sur appareil, chemin des flèches : brouillon réel -> barre en bas avec
Annuler/Enregistrer, corps intact ; ordre rétabli -> « L'ordre est revenu à
celui de la box » avec le seul « Terminer » ; Terminer -> barre absente. Banc
inchangé, aucune écriture.
Et la note gardée sur `draftIsNoOp` : « rien à enregistrer » veut dire « rien
par rapport à ce que tu as vu ». Comparer à l'état vivant conclurait à la place
de l'utilisateur sur des faits qu'il n'a pas vus. Le référentiel de la
CONCLUSION et celui de l'ÉCRITURE sont deux choses, et il faut les deux.
Capture : docs/captures/2026-09-08_barre-en-pied-terminer.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
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
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
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
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
« 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
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
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
`+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
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
Deux retours d'usage, tous deux sur la même carte.
1. « Placer en dernier » poussait « Configurer » hors de sa place. Il rejoint « Renommer »
sur une ligne à part : ce sont les deux gestes qui ne touchent pas au mécanisme, et un
bouton d'action principale ne se fait pas déplacer par un geste secondaire.
2. Quitter l'écran avec des modifications non enregistrées ne demandait rien — et ce
silence cachait pire qu'une perte : le brouillon vit dans LoadConfigProvider, pas dans
l'écran, donc il SURVIT à la navigation. On repartait en laissant une configuration
modifiée invisible, qui réapparaissait au retour sans contexte, ou disparaissait à la
première relecture de la box (LoadConfigChanged, reconnexion) sans que personne ait
choisi. Les deux issues sont mauvaises pour la même raison : l'utilisateur ne sait pas
ce qu'il laisse derrière lui.
GardeBrouillon (PopScope) s'interpose sur les DEUX écrans qui partagent ce brouillon — la
liste des charges et l'écran de mécanisme. L'alerte dit l'état de la BOX, pas seulement
celui de l'écran : « vos changements n'ont pas été envoyés, elle porte toujours la
configuration d'avant ». Deux issues, toutes deux explicites : rester, ou quitter en
abandonnant — un brouillon fantôme qui ressort trois écrans plus loin est pire qu'une
modification perdue qu'on a accepté de perdre.
Aucune écriture n'est proposée depuis la boîte de dialogue : enregistrer depuis une sortie
ferait partir une configuration que personne n'a relue.
Quatre tests, dont le contrôle négatif — sans brouillon, on sort sans être interrogé. Une
alerte qui se déclenche pour rien apprend à passer outre sans lire.
235 tests (+4), flutter analyze inchangé à 27 remarques.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
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
Écrit AVANT le déploiement du moteur, pour qu'il n'y ait pas de trou.
`measuredW` portait trois régimes sous un seul nom et jetait la distinction : une borne
rendait une mesure, un routeur de relais une mesure OU son palier nominal selon que ses
relais mesurent, un SG-Ready une puissance toujours déclarée. C'est la condition C2 de
LM-1009. Le régime se publie désormais :
measurement: { source: "meter" | "device" | "none", powerW }
Les trois pièges d'affichage du §7, figés par test :
· `none` est un état POSITIF — « cette charge n'est pas mesurable ». Il autorise à MASQUER
le bloc au lieu d'afficher un vide, et sépare enfin « pas mesurable » de « compteur en
panne ». Aucune puissance nominale n'est allée la chercher dans mechanism.* pour combler :
elle produirait un écart identiquement nul à tous les cycles, soit un « tout concorde »
permanent qui ne signale plus rien.
· {source: "device", powerW: 0} est une MESURE. Une borne branchée qui ne charge pas mesure
zéro watt. Si la source basculait à l'extinction, le bloc apparaîtrait et disparaîtrait au
rythme du thermostat.
· L'écart n'a pas la même valeur selon la source, et l'écran le DIT : sous `device`, il
montre la désobéissance mais une concordance ne prouve rien — plusieurs wallbox renvoient
leur consigne ; sous `meter`, la mesure est indépendante de l'organe commandé et la
concordance devient une preuve.
Le compteur dédié se décide sur `measurement.source`, JAMAIS sur `adapter` :
source == device → réglage avancé, replié (l'appareil publie déjà)
sinon → proposé, visible
La règle couvre SG-Ready sans avoir à le connaître — `none` par construction, LM-1105-b : un
contact de signal ne voit pas le courant de la machine, et pour une PAC le compteur dédié est
la SEULE voie vers une mesure. Elle couvre aussi la topologie de relais mixte, `none` elle
aussi, qu'un « masqué dès que la charge mesure, sauf SG-Ready » aurait manquée. Redéduire le
régime du mécanisme remettrait ici la déduction que le moteur vient d'en retirer.
⚠️ Une distinction que le contrat n'a pas à porter mais que l'écran doit faire : une charge
ABSENTE de la télémétrie n'est pas `none`. `none` est publié ; l'absence ne dit rien. Les
confondre annoncerait « seule voie vers une mesure » sur une borne qui publiera très bien la
sienne au prochain cycle.
⏳ Alias temporaire `measuredW` en repli, avec sa date de retrait : dès que +etm26 est sur
.75 (entrée TODO, sonde nommée). `measurement` gagne quand les deux sont là.
204 tests (+16), flutter analyze inchangé à 27 remarques. Prêt pour le déploiement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
Deux suites directes du geste réel du 2026-08-28 sur le banc.
── 1. Une vérification qui arrive trop tard ne prouve rien ───────────────────
Sur le banc, la suppression a été vérifiée à la main, APRÈS un autre geste : entre-temps un
« Enregistrer » avait regroupé et renuméroté (chauffe-eau 1 → 3). Il est alors devenu
impossible de dire laquelle des deux écritures avait déplacé les rangs. Le journal de la box
montrait deux persistances à deux minutes d'écart, rien de plus.
`removeLoad` relit donc la configuration IMMÉDIATEMENT après l'écriture — sans attendre
LoadConfigChanged, qui laisserait une fenêtre où un autre geste s'intercale — et compare
clé par clé ce qu'elle a envoyé à ce que la box porte (diffPayloads, fonction pure). Les
survivantes doivent être identiques, rangs compris : c'est le contrôle de l'aller-retour
neutre appliqué à une écriture qui RETIRE.
Le message le dit : « Relu sur la box : les autres sont identiques clé par clé, rangs
compris », ou l'écart en rouge, nommé champ par champ. Sur le seul geste de l'app qui
détruit de la configuration, « la box a dit oui » ne suffisait pas.
La comparaison est structurelle : relays, sgReady et powerLevels sont des listes et des
objets, que `==` compare par référence — l'alerte aurait crié à chaque suppression, donc
plus personne ne l'aurait lue.
── 2. Le sélecteur de compteur était plus restrictif que la box ─────────────
Il filtrait sur l'interface smartmeter. Le moteur n'en regarde AUCUNE : findConfiguredThing
puis hasState("currentPower") (attachMeasurements, LM-1101). Sur .75, la classe wallbox
déclare smartmeter, la classe trydan non — la V2C était donc impossible à désigner comme
son propre compteur, alors que la box l'aurait accepté et que c'est le seul moyen de voir
« mesuré » à côté de « alloué » sur sa carte.
Le sélecteur accepte maintenant, en plus des smartmeter, les Things d'une interface
élargie qui publient l'état exigé par le moteur — ici evcharger + currentPower. Il s'élargit
là où la box est plus large, pas partout : une borne qui ne publie aucune puissance reste
dehors, sans quoi on écrirait une configuration muette sans erreur.
⚠️ Ce n'est pas la fin de l'écart : la classe trydan devrait déclarer smartmeter, ce qui
suppose un CUMUL d'énergie qu'elle ne publie pas — même manque que celui qui a bloqué
LM-1009. Lot plugin noté au TODO avec cette raison.
── Banc ──────────────────────────────────────────────────────────────────────
Chauffe-eau remis au rang 1 (lire → patcher → réécrire, seul `priority` touché, relecture
vérifiée : la box porte exactement ce qui a été envoyé). Ordre courant : chauffe-eau,
V2C, wallbox simulée, PAC.
188 tests (+7), flutter analyze inchangé à 27 remarques.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
Deux manques trouvés en usage réel sur téléphone, contre .75.
── 1. Retirer une charge dont l'appareil n'existe plus ────────────────────────
Le plugin crée d'office une entrée par borne détectée mais ne la retire pas quand le Thing
s'en va : la Terra AC supprimée de nymea tenait encore le rang 4 deux jours plus tard, et
l'app savait le dire sans savoir y remédier.
Le bouton n'apparaît que sur THING_MISSING. Sur une charge vivante le geste n'aurait pas de
sens — le plugin recrée l'entrée au cycle suivant, et un bouton qui se défait tout seul
ferait douter de la box.
Le chemin est celui qui était demandé, et pour la raison qui était donnée :
· LIRE (GetLoadConfig, frais — pas l'état en mémoire, la box a pu changer d'avis)
· PATCHER (payloadWithoutLoad : les autres charges recopiées VERBATIM, champ par champ)
· RÉÉCRIRE (SetLoadConfig, qui remplace l'ensemble)
Jamais une reconstruction depuis le modèle typé : un champ qu'une box plus récente porterait
et que l'app ne connaît pas serait perdu pour TOUTES les charges d'un coup, en silence. Les
rangs ne sont pas renumérotés — le trou laissé (1,2,3,5) ne change l'ordre de personne,
alors qu'une renumérotation réécrirait le rang de charges que personne n'a touchées.
Rien à moitié écrit : un refus laisse la configuration locale intacte (aucune adoption avant
confirmation) et la box refuse l'ensemble ou l'accepte en entier. Le geste est refusé tant
qu'un brouillon est en cours — sinon la suppression emporterait des modifications non
enregistrées. La confirmation annonce ce que l'utilisateur ne peut pas deviner : retirer une
charge réécrit toutes les autres.
── 2. Le mode de recharge, un seul lieu d'écriture ───────────────────────────
Les trois boutons quittent la tuile des favoris pour l'écran « Configurer » de la borne :
c'est l'écran de mise en service, celui où on découvre qu'une borne neuve est en
ChargingModeNormal. Deux endroits qui écrivent SetChargingInfo seraient deux comportements
qui divergent.
La tuile des favoris garde la LECTURE et l'avertissement « hors arbitrage » : c'est le
client qui ouvre ses favoris, et il doit voir que sa borne n'est pas pilotée même s'il doit
aller ailleurs pour le corriger.
⚠️ Bug latent trouvé en déplaçant : setChargingInfo écrivait toujours sur la PREMIÈRE borne
configurée (_findEvChargerId). Sur le banc qui en porte trois, régler la V2C depuis son
propre écran aurait réglé la wallbox simulée. La borne est maintenant visée explicitement,
et evChargerFor() réconcilie les deux graphies d'identifiant (§9 du contrat de données).
181 tests (+12), flutter analyze inchangé à 27 remarques.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
Deux défauts trouvés sur le téléphone en dix minutes d'usage, contre .75. Aucun des 164
tests ne pouvait les voir : le premier vit dans une méthode privée de widget, le second est
une question de sens, pas de valeur.
1. « La puissance nominale est requise et doit être supérieure à 0 W » — sur une BORNE.
_valider() faisait tomber adapter: evcharger dans la branche des charges modulables, donc
l'écran refusait d'enregistrer un simple renommage, deux blocs sous sa propre carte qui
explique qu'une borne ne porte ni contacts, ni paliers, ni puissance nominale. La règle
n'est pas supprimée, elle est rendue à son périmètre — contrôle négatif à l'appui.
2. Une SONDE DE TEMPÉRATURE sur une borne de recharge. sensorThingId est générique au
schéma — la box l'accepte sur n'importe quelle charge — mais le moteur ne le lit que pour
publier une température (attachMeasurements, LM-1103), et une borne ne rend aucun service
thermique : le champ ne menait nulle part. Retiré pour les bornes, gardé partout ailleurs.
Le compteur dédié, lui, RESTE sur une borne, et l'écran dit enfin à quoi il sert : c'est le
seul chemin par lequel measuredW apparaît dans la télémétrie (le moteur lit currentPower du
Thing désigné par meterThingId, et de personne d'autre). Sur une borne qui publie
elle-même currentPower, on la désigne comme son propre compteur — c'est ce qui fait
apparaître « mesuré » à côté de « alloué » sur sa carte.
Et l'écran cesse de renvoyer vers la carte véhicule du tableau de bord, supprimée ce matin :
le mode de recharge se règle depuis la tuile « Borne EV » des favoris, seul endroit de l'app
qui écrit SetChargingInfo.
Cinq tests de widget montent l'écran sur la configuration réelle du banc. Piège de harnais
noté dedans : l'écran est un ListView, qui ne construit que son viewport — sur 600 px, la
moitié des cartes n'existe pas, et chercher une absence y réussit toujours. Surface de test
portée à 4000 px.
169 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
Elle reste : la retirer aurait laissé un trou dans une liste que le client a composée
lui-même. Elle affiche maintenant ce que la box publie, et là où il n'y a rien à lire,
elle le dit.
Ce qui est lu, et sa source
· branché / en charge ← états pluggedIn, charging du Thing
· puissance ← état currentPower du Thing
· mode ← GetChargingInfos + notification ChargingInfoChanged
Absent = « non publié », jamais zéro. Un mode que l'app ne connaît pas se lit « mode non
reconnu (X) » plutôt que d'être deviné, et aucun bouton n'est allumé tant que la box n'a
pas dit ce qu'elle porte.
L'écriture restitue le refus, sinon elle n'existe pas. Les pastilles de 7 à 10 px qui
avalaient les codes d'erreur de setChargingInfo passent à 44 px, montrent l'attente, et
affichent le refus tel que la box le nomme. Et l'app ne tient plus d'état de mode local :
après une écriture acceptée, elle RELIT GetChargingInfos. Elle se souvenait de ce qu'elle
avait demandé, ce qui n'est pas ce que la borne porte.
La fiction perd sa porte d'entrée. chargingMode, chargingPower (3,6 kW) et
solarSourcePercent (82 %) sont SUPPRIMÉS de EnergyData : trois valeurs par défaut que
_updateEnergyData ne touchait jamais. Ce que la box publie sur une borne vit désormais
dans EvChargerLive, champ par champ, nullable.
⚠️ Et le Sankey du tableau de bord en tirait un flux « Voiture » permanent à 3,6 kW,
voiture débranchée. Il est branché sur la mesure de la borne — « non lu » quand il n'y en
a pas. Mais la même vue invente encore PAC = 38 % de la maison, Eau chaude = 18 %,
Autres = 22 %, dont un seul marqué « estimé » : entrée 🔴 au TODO, avec la source réelle
(GetLoadTelemetry publie measuredW par charge : 1500 W et 800 W ce midi). Brancher le
Sankey dessus est une refonte de l'écran d'accueil, pas un correctif — à décider.
Deux attentes tombent, une reste (relevé Integrations.GetThings sur .75, cet après-midi)
· currentL1/L2/L3 est DÉPLOYÉ (5,86 / 5,94 / 6,02 A) — powerL1/L2/L3 a disparu
· phaseCount EXISTE sur la Trydan (3) — la note qui le disait absent était en retard
· sessionEnergy existe (4,229 kWh) MAIS vaut exactement chargeEnergy : c'est ce qu'on
attend d'une session sans interruption, donc ça ne prouve rien. LM-1009 §C1 reste
entière tant qu'une remise à zéro de chargeEnergy n'a pas été vue avec un
sessionEnergy qui continue.
Le brief moteur dit désormais du masquage WRITE_FAILED que ce n'est pas « ne se produit
pas » mais « ne s'est pas encore produit » : il attend une charge variable qui perd son
Thing, et le banc n'a pas eu ce cas.
154 tests (8 nouveaux sur la tuile, dont les cibles tactiles et la restitution du refus),
flutter analyze à 27 remarques, aucune nouvelle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
L'entrée 🔴 du TODO. Aucune des quatre tuiles n'était journalière : deux pourcentages
instantanés, deux compteurs cumulés depuis l'origine dont deux portaient le mot
« aujourd'hui ». Elles montrent désormais la journée, sous un en-tête qui le dit une fois.
MÉTHODE : une DIFFÉRENCE de compteurs entre minuit local et maintenant, jamais une
intégration des puissances. La première est une mesure, prise sur les mêmes compteurs que
ceux du fournisseur ; la seconde est une estimation qui suppose que chaque échantillon
représente son intervalle. Mesuré sur .75 : l'intégration rend 77,70 kWh là où la différence
rend 78,31 — 0,8 % d'écart, systématiquement à la baisse. Il n'y a donc PAS de repli
silencieux de l'une vers l'autre : quand la mesure n'est pas exploitable, le type le dit.
DEUX BUGS D'UNITÉ, trouvés en cherchant à savoir ce que valaient les compteurs. Aucune
documentation ne tranchait, donc recoupement sur machine : l'intégration des puissances (en
W) rend 77 697 Wh quand la différence de totalProduction rend 78,310. Le rapport est mille —
LES COMPTEURS SONT EN kWh, et le modèle Dart les nommait Wh.
1. L'écran Énergie divisait ces kWh par mille pour les afficher : une journée de 78 kWh
s'y lisait « 78 Wh », sous un axe intitulé « Bilan énergétique (Wh) ». Invisible sans
connaître l'ordre de grandeur attendu.
2. La simulation accumulait des Wh : un écran juste en simulation aurait cassé d'un
facteur mille contre une vraie box.
LE CONTRÔLE QUI VALIDE LE BILAN : production + soutiré == consommation + injecté. C'est le
seul recoupement disponible sur des compteurs qu'on ne peut pas vérifier autrement, et il
tient EXACTEMENT sur .75 — 112,244 des deux côtés sur 21,75 h. Faux, il désigne un compteur
cassé ou une remise à zéro, et la carte le signale au lieu de présenter comme une mesure ce
qui ne se recoupe pas.
Les trois pièges du TODO n'ont pas été repayés : bornes à minuit LOCAL via
GetPowerBalanceLogs · décodage délégué à fetchPowerBalanceLogs, qui porte déjà les bonnes
clés sans préfixe currentPower · plausibilité vérifiée sur les DEUX bornes avant de
soustraire, parce que deux valeurs absurdes peuvent avoir une différence d'apparence saine.
Plus un quatrième trouvé en chemin : un compteur qui RECULE (redémarrage en milieu de
fenêtre) donne un bilan négatif, et un max(0, …) le rendrait faux en silence.
Rien ne s'affiche jamais à zéro quand c'est inconnu. Sans production, le taux
d'autoconsommation est null et non 0 % — à 3 h du matin, « 0 % » se lirait comme un défaut
alors qu'il n'y a rien à autoconsommer.
VÉRIFIÉ SUR APPAREIL contre .75, en lecture seule (le banc est en préparation V2C, rien n'a
été écrit) : 32 échantillons depuis minuit, prod 28,37 · conso 26,55 · import 11,95 ·
export 13,77 kWh, identité OK, et les tuiles affichent 51 % / 55 % / 13,8 / 11,9.
Le passage sur appareil a trouvé ce qu'aucun des 138 tests unitaires ne pouvait voir : LES
QUATRE LIBELLÉS DÉBORDAIENT — « Autoconsomm… », « Vers réseau · aujou… ». Le suffixe
« aujourd'hui » répété quatre fois mangeait le nom de la grandeur ; il est dit une fois en
en-tête. Le harnais d'appareil échoue désormais sur tout libellé finissant par « … ».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
/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
+etm23 rend à chaque borne détectée une entrée GetLoadConfig, et l'invariant
loads[] ⊆ GetLoadConfig est rétabli. Sur .75 : 2 entrées → 4. L'app s'appuyait déjà sur cet
invariant pour résoudre libellé, domaine et rang de toute charge publiée.
Vérifié sur machine avant d'écrire quoi que ce soit :
- l'aller-retour verbatim reste NEUTRE avec les quatre entrées (loadconfig_roundtrip.dart,
verdict IDENTIQUE) ;
- une entrée evcharger porte id/label/domain/priority/enabled, mode dynamic, et AUCUNE
charge utile de mécanisme.
Trois défauts que ce contrat révèle côté app :
1. isVariableLoad était défini par défaut (« ni relais ni sg-ready »), donc une borne y
tombait. L'écran de mécanisme lui proposait une puissance nominale, des paliers et des
temporisations — des réglages qui n'existent pas pour elle et dont l'écriture partirait
quand même. Pire, le sélecteur offrait de la basculer en routeur de relais : une bascule
que basculerMecanisme() sait construire et qu'aucun écran ne sait défaire. Une borne a
maintenant sa section, en lecture seule, qui dit pourquoi il n'y a rien à régler.
2. BorneSection existait pour rattraper des bornes qui n'apparaissaient nulle part. Elles
apparaissent maintenant dans leur domaine : la section les afficherait DEUX fois, en
affirmant qu'elles n'ont « ni rang ni domaine » et qu'elles ne passent pas par
SetLoadConfig. Trois affirmations devenues fausses. Retirée ; son contenu de runtime
vit désormais dans l'écran de mécanisme, où il a un sens.
3. Le rang par défaut d'une borne est un ARTEFACT, et l'écran le disait comme un réglage.
À la création le plugin donne « le plus petit rang existant moins un, borné à 1 » ;
le chauffe-eau occupant déjà le rang 1, l'intention « en tête » dégénère en égalité —
trois charges à 1 sur le banc. Le tri reste total (l'id départage), donc l'ordre est
reproductible, et c'est précisément ce qui le rend crédible : l'installateur lit un
classement plausible et n'y touche pas, validant un ordre que personne n'a posé. Une
note le nomme, et disparaît dès le premier vrai réglage.
Le glissement, lui, n'a demandé aucun cas particulier — c'était tout l'intérêt d'avoir une
seule liste. Vérifié par test, y compris l'inversion borne / charge pilotée.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
`batteryLevelConsideration` ne gouverne aujourd'hui que la recharge du véhicule. Une
fois la règle montée dans le budget, il gouvernera TOUTE l'installation : sous le
seuil le budget de surplus est annulé — pas réduit — donc ni ECS, ni PAC, ni bornes.
Le défaut d'usine est 0,9. Le banc `.75` est exactement dans ce cas : seuil 0,9,
batterie à 50 %. Le symptôme chez un client serait le pire qui soit — un HEMS qui ne
pilote plus rien par grand soleil, sans erreur, sans panne, rien à lire dans un
journal. Un paramètre qui peut produire ça n'est pas un paramètre d'usine.
- `BatteryReserveCard` lit et écrit le réglage, et ALERTE quand le seuil dépasse le SOC
réellement atteint — le cas silencieux. Elle n'invente aucune valeur par défaut : si
la box ne l'expose pas, la carte se tait plutôt que d'afficher un curseur mort.
- La portée future est dite sur la carte : régler ce seuil « pour la voiture »
couperait le reste demain.
- Clé ARB `decisionBatteryReserve` posée, et `kCodesReserveBatterie` volontairement
VIDE côté `telemetry_text` : le crochet attend le code que publiera le moteur, on ne
devine pas son nom.
Vérifié sur appareil contre `.75` : réglage lu (0.9 / SOC 50 %), carte affichée,
campagne verte. L'assertion des cartes de charge fait désormais DÉFILER avant de
compter — dans un `ListView` ce qui n'est pas à l'écran n'est pas construit, et
chercher sans défiler ne prouvait que la hauteur de l'écran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff