diff --git a/CLAUDE.md b/CLAUDE.md index 53e2c1b..c176382 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ nymeaService.call('NymeaEnergy.SetChargingInfo', { 'chargingMode': 'ChargingModeEco', // requis, énumération ci-dessous 'targetPercentage': 80, // o: Uint — charge cible 'endDateTime': 1786250400, // o: Uint — horodatage, PAS une chaîne - 'repeatDays': [1, 2, 3, 4, 5], // o: Int[] + 'repeatDays': [1, 2, 3, 4, 5], // o: Int[] — 1 = LUNDI … 7 = dimanche, jamais 0 'assignedCarId': carId, // o: Uuid 'spotMarketChargingEnabled': false, // o: Bool 'dailySpotMarketPercentage': 30, // o: Uint @@ -247,7 +247,7 @@ batteryGreen boostRed pvGreen minPvBlue accentTeal --- -## Télémétrie d'arbitrage — deux pièges de lecture +## Télémétrie d'arbitrage — les pièges de lecture **1. `funding` — ne jamais sommer `allocatedW` sans filtrer.** Depuis `+etm22` une borne peut figurer dans `loads[]` au titre du surplus **ou** du réseau (échéance, tarif @@ -256,15 +256,45 @@ dynamique). L'identité vérifiable est : *somme des `allocatedW` financés au s sans filtrer donne un écart que rien ne permet d'interpréter. Omis en mode dégradé — sans plan, pas de financement. Cf. `LoadTelemetry.surplusAllocatedW`. -**2. ⚠️ Pour une BORNE, `allocatedW` et son motif sont une INTENTION, pas un ordre.** -`adjustEvChargers()` re-décide le surplus après le waterfall et passe outre le refus de -l'arbitre : relevé sur `.75`, l'arbitre publie « 0 W / `BELOW_MIN_POWER` » et le proxy -commande `ON` à 6 A dans le même cycle. L'état réel se lit dans -`mechanism.chargingEnabled` / `mechanism.currentA`, qui viennent du Thing. **Ne pas écrire -« la borne ne charge pas parce que le budget est sous le plancher » à partir du seul -motif.** La réserve **ne vaut pas** pour l'ECS et la PAC, où l'allocation publiée **est** -la commande. Elle tombe avec le lot plugin **3g-2** ; côté app, elle est portée par -`LoadTelemetryEntry.allocationIsCommand`. +**2. `allocatedW` est ce qui a été COMMANDÉ — jamais ce que la charge tire.** +La mise en garde « pour une borne, ce n'est même pas une commande » est **levée** : +depuis `+etm23`, `adjustEvChargers()` ne commande plus, l'arbitre est seul (mesuré : 9 +commandes à la borne, 9 par l'arbitre, 0 par le proxy). Mais ce qui devient vrai, c'est +que **personne d'autre ne commande** — pas que la charge obéit. Un plafond matériel, un +véhicule qui refuse, un câble débranché font toujours diverger l'ordre et la réalité. +L'écart se lit dans `measuredW` et dans la charge utile `mechanism` (`chargingEnabled`, +`currentA`, `phaseCount`, `pluggedIn`), **jamais dans `allocatedW`**. + +**3. `EV_GRID_START` partage une allocation entre les DEUX compteurs du budget.** C'est le +seul motif qui le fasse : `budgetW` vient du surplus et entre dans `budget.allocatedW`, +`gridW` est acheté au réseau et entre dans `budget.evReservedW`, avec +`budgetW + gridW == allocatedW`. Toute réconciliation doit traiter cette ligne à part — +cf. `LoadTelemetryEntry.surplusShareW`. Motif **jamais vu sur machine** au 2026-08-27 : +la clé est prête, rien n'est bâti autour. + +**Bornes en configuration (`+etm23`)** — toute borne détectée reçoit d'office une entrée +`GetLoadConfig` : `adapter: "evcharger"`, `mode: "dynamic"`, `domain: "ev"`, plus +`label` / `priority` / `enabled`. **Aucune charge utile de mécanisme** — ni `relays`, ni +`sgReady`, ni `powerLevels`/`maxPowerW`/`minPowerW`, ni `minOnS`/`minOffS` : les limites +d'une borne viennent du Thing et changent avec le véhicule branché. L'aller-retour verbatim +reste neutre (vérifié sur `.75` le 2026-08-27, quatre entrées). Son rang est un +`priority` ordinaire — **il n'y a pas de second système de priorité pour les bornes**, et +le glisser-déposer marche sur une seule liste. + +> ⚠️ **Le rang par défaut d'une borne n'est pas tranché.** À la création, elle reçoit « le +> plus petit rang existant moins un, borné à 1 » ; quand une charge occupe déjà le rang 1, +> l'intention dégénère en **égalité** (sur le banc : trois charges à 1). Le tri reste total +> — l'identifiant départage — donc l'ordre est reproductible, mais **reproductible n'est +> pas choisi**. Ne pas présenter l'ordre affiché avant réglage comme un choix. + +**Une borne configurée absente de `loads[]`** = *hors arbitrage en ce moment* — aucun +véhicule branché (depuis `+etm24`), pas de voiture assignée, ou mode manuel. Jamais +« perdue », jamais « désactivée ». + +**Aucun SOC de véhicule n'existe dans ce contrat.** Vérifié sur `.75` : sur 58 classes, +aucune classe `evcharger` ne déclare d'état de charge et aucune ne porte l'interface `car`. +Le seul `batteryLevel` est celui de la **batterie de la maison**. Ne pas l'afficher en face +d'une cible de recharge : c'est une autre grandeur. **Motif `BATTERY_RESERVE`** — params `socPercent`, `reservePercent`, `withheldW`, tous entiers **déjà en pourcentage** (ne pas confondre `reservePercent` = 40 avec diff --git a/docs/BRIEF_depuis_plugin.md b/docs/BRIEF_depuis_plugin.md index f9565a2..cdd6e09 100644 --- a/docs/BRIEF_depuis_plugin.md +++ b/docs/BRIEF_depuis_plugin.md @@ -1,149 +1,140 @@ -# Brief — du moteur vers l'app +# Brief agent app — ce que 3g-2 change pour l'écran -Ce que l'agent plugin (`etm-powersync-energy-plugin-etm`) constate et qui **change quelque -chose côté app**. Une section datée par lot. Le plus récent en haut. +**Émis par** : l'agent plugin, après 3g-2 · **Date** : 2026-08-27 +**Banc** : `.75`, plugin `1.15.2+etm24`, nymea `1.15.2+202606191336~trixie1` +**Contexte** : `docs/RELEVE_3g2.md` (ce dépôt) porte les traces machine, cycle par cycle. +**Contrat** : `INTERFACE.md` fait autorité — ce brief ne fait qu'en signaler les mouvements. + +> Quatre points. Le deuxième **retire une mise en garde** que l'app portait, et il est le plus +> intéressant pour vous : du code conditionnel peut disparaître. --- -## 2026-08-26 — passage machine 3g-1 sur le banc `.75` +## 1. `loads[] ⊆ GetLoadConfig` est RÉTABLI -**Contexte** : `powersync-energy-plugin-nymea 1.15.2+etm22` déployé sur `.75`. Relevé complet -dans le dépôt du plugin, `docs/RELEVE_3g1.md`. +3g-1 avait rompu l'invariant sans le signaler : les bornes entraient dans `loads[]` de +`GetLoadTelemetry` **sans figurer dans aucune `GetLoadConfig`**. C'était exactement la charge +fantôme que le lot B-bis avait supprimée, réapparue par l'autre bout — et vous vous appuyez sur +cet invariant pour résoudre libellé, domaine et rang de chaque charge publiée. ---- - -### 1. `SetChargingInfo` substitue `targetPercentage = 80` à un champ absent — **en silence** - -**Envoyer un `chargingInfo` sans `targetPercentage` ne laisse pas la valeur en place : elle est -remplacée par 80.** Constaté en restaurant le banc — le champ valait `0`, j'ai réécrit l'objet -sans lui, il valait `80` à la relecture. - -**Cause** : `ChargingInfo` est reconstruit intégralement à chaque écriture (`unpack`), -et son membre C++ porte `m_targetPercentage = 80` comme valeur par défaut. Un champ absent du -JSON n'est donc pas « inchangé » : il **retombe sur le défaut**. Cela vaut pour tout l'objet, -`targetPercentage` n'est que le cas où le défaut n'est pas neutre. - -**Et la substitution est muette.** Vérifié sur machine, journal du banc à 17:35:56 : - -- la réponse RPC est `EnergyErrorNoError` — aucune erreur, aucun avertissement ; -- le seul avertissement du chemin d'écriture ne se déclenche que pour `targetPercentage > 100` ; -- la seule trace au journal est une ligne de **debug** qui imprime `Target percentage: 80`, - c'est-à-dire le **résultat** — jamais le fait qu'un champ était absent ; -- la notification `ChargingInfoChanged` qui suit porte 80, comme si l'utilisateur l'avait choisi. - -> **Ce que l'app doit en faire.** Toujours envoyer `targetPercentage`, même inchangé — donc -> lire l'objet, patcher le champ voulu, réécrire l'objet complet (le même chemin que pour -> `priority` dans `LoadConfig`). Et **ne jamais afficher `targetPercentage` comme une cible -> choisie par l'utilisateur** tant que l'écran n'a pas lui-même écrit cette valeur : 80 peut -> être une valeur héritée d'une écriture qui ne parlait pas de cible du tout. - -Le risque concret : un écran qui change le seul mode de charge repose une cible de 80 % à -l'insu de l'utilisateur, sans que rien ne le signale. - ---- - -### 2. `GetPowerBalanceLogs` en `SampleRate15Mins` — **RIEN À SIGNALER, l'alerte était fausse** - -J'avais signalé que ce taux rendait des échantillons tous à zéro. **C'était mon erreur de -lecture, pas un défaut de la box.** Je la consigne quand même pour qu'elle ne circule pas. - -Ce qui s'est passé : ma sonde lisait les clés `currentPowerProduction` / `currentPowerConsumption` -alors que les entrées de log portent **`production`, `consumption`, `acquisition`, `storage`** — -sans le préfixe `currentPower`, contrairement à `GetPowerBalance`. Tout tombait donc sur le -défaut `0`. - -**Vérifié après coup, dans les deux sens :** - -- par RPC, mêmes bornes de temps, avec les bonnes clés : **51 échantillons, 51 non nuls** ; -- dans la base du banc (`/var/lib/nymea/energylogs.sqlite`), la table `powerBalance` porte - **7 908 lignes à `sampleRate = 15`**, du 2026-06-05 à aujourd'hui 16:30, valeurs réelles. - Les autres taux (1, 60, 180, 1440, 10080, 43200) sont peuplés aussi. - -> **Conséquence pour l'app : l'historique 90 jours n'est bloqué par rien de ce côté.** Le seul -> piège est le nom des champs — **les entrées de log ne portent pas le préfixe `currentPower`**, -> à la différence de `GetPowerBalance`. Un modèle qui réutilise les mêmes clés pour les deux -> appels lira des zéros sans la moindre erreur. C'est exactement ce qui m'est arrivé. - ---- - -### 3. Contrat neuf — `loads[].funding`, et les bornes entrent enfin dans `loads[]` - -#### 3.1 Les bornes franchissent la frontière RPC (`+etm22`) - -Jusqu'ici `buildTelemetry()` **sautait les bornes** : elles étaient arbitrées, leurs décisions -apparaissaient au journal du plugin, mais `GetLoadTelemetry` n'en publiait aucune. L'écran ne -pouvait donc pas les voir. C'est corrigé. - -`loads[]` contient désormais les bornes de recharge, avec leur `decision`, leur `mechanism` -(`kind: "evcharger"`, `chargingEnabled`, `currentA`, `phaseCount`, `pluggedIn`) et leur -`faultCode` le cas échéant. - -Les motifs EV **`EV_SURPLUS`, `EV_ECO_MIN`, `EV_IDLE` ne sont plus émis** : une borne reçoit -maintenant les motifs communs à tous les mécanismes — `SURPLUS_SETPOINT`, `BELOW_MIN_POWER`, -`SURPLUS_INSUFFICIENT`, `LOAD_UNAVAILABLE`. Les trois codes restent rendus côté plugin pour -qu'un journal ancien reste lisible, mais plus rien ne les produit. `EV_SPOT_MARKET` et -`EV_DEADLINE` subsistent. - -#### 3.2 Le champ `funding` — `"surplus"` ou `"grid"` - -**Nouveau, optionnel (`o:`), présent dès qu'il y a un plan.** Il devient nécessaire du seul fait -que les bornes sont dans `loads[]`, car deux d'entre elles peuvent y être pour des raisons -différentes : - -| `funding` | d'où vient l'allocation | où elle est comptée | -|---|---|---| -| `"surplus"` | la cascade (waterfall PV) | dans `budget.allocatedW` | -| `"grid"` | le proxy — échéance de départ, tarif dynamique — qui **soutire au réseau** | dans `budget.evReservedW` | - -> **Sommer `loads[].allocatedW` sans filtrer sur `funding == "surplus"` donne un écart avec -> `budget.allocatedW` que rien ne permet d'interpréter** : défaut du moteur, ou borne servie au -> réseau ? L'identité à vérifier est donc : **somme des `allocatedW` des charges financées au -> surplus == `budget.allocatedW`**. - -**Omis en mode dégradé** : sans plan, il n'y a pas de financement. Règle maison habituelle — -omis, jamais nul. - -#### 3.3 ⚠️ Mise en garde — pour une borne, l'allocation publiée est une INTENTION, pas un ordre - -**`adjustEvChargers()` re-décide le surplus après le waterfall et passe outre le refus de -l'arbitre.** Observé sur le banc, deux lignes consécutives du même cycle (17:34:00) : +**Depuis `+etm23`, toute borne détectée reçoit d'office une entrée `LoadConfig`.** Sur le banc, +`GetLoadConfig` est passé de **2 à 4 entrées** au premier cycle après la mise à jour : ``` -[Arbitre] "{09e520fd…}" → "Budget 1096 W sous le premier palier … — 0 W" ----------------- Adjusting chargers ---------------- -Executing action Simulated wallbox to power: ON, Charging current: 6A, Issuer: Surplus +chauffe-eau adapter=relay-router prio=1 domain=ecs +PAC banc adapter=sg-ready prio=2 domain=heating +Simulated wallbox adapter=evcharger prio=1 domain=ev ← nouvelle +Terra AC Charger (TCP) adapter=evcharger prio=1 domain=ev ← nouvelle ``` -L'arbitre refuse et publie `BELOW_MIN_POWER` ; le chemin d'exécution du proxy commande `ON` à -6 A une milliseconde plus tard. **Tant que ce n'est pas traité (lot 3g-2, côté plugin), ce que -la télémétrie publie pour une borne n'est pas ce qui est commandé.** +Forme d'une entrée `evcharger` : `id` (le ThingId de la borne), `adapter: "evcharger"`, +`mode: "dynamic"`, `domain: "ev"`, plus `label` / `priority` / `enabled`. **Aucune charge +utile** — pas de `relays`, pas de `sgReady`, pas de `powerLevels`/`maxPowerW`/`minPowerW`, pas +de `minOnS`/`minOffS`. Les limites d'une borne viennent du Thing et changent avec le véhicule +branché ; les déclarer en configuration en ferait une seconde source de vérité. -> **Ce que l'app doit en faire.** Pour les bornes — et seulement pour elles —, ne pas présenter -> `allocatedW` et son motif comme l'état de la borne. Ce sont **la décision de l'arbitre**, ce -> qui n'est pas la même chose que ce que la borne fait. L'état réel se lit dans -> `mechanism.chargingEnabled` / `mechanism.currentA`, qui viennent du Thing. -> -> Concrètement : ne pas écrire « la borne ne charge pas parce que le budget est sous le -> plancher » à partir du seul motif. Les deux peuvent se contredire aujourd'hui, et c'est -> l'écran qui aurait l'air faux. -> -> Cette réserve **ne vaut pas** pour l'ECS, la PAC et les autres charges pilotées : là, -> l'allocation publiée **est** la commande. +**Rien à faire de votre côté pour que ça marche** : l'aller-retour `GetLoadConfig` → +`SetLoadConfig` verbatim est **neutre**, vérifié sur machine — l'objet relu est identique et +aucun adaptateur n'est reconstruit. + +**Le sens de l'inclusion reste le même** : `GetLoadConfig` peut contenir des charges absentes de +`loads[]`. Pour une borne, ça veut dire *hors arbitrage* — pas de voiture assignée, mode manuel, +ou **aucun véhicule branché** (ce dernier cas depuis `+etm24`). Une borne configurée absente de +`loads[]` se lit « pas arbitrée en ce moment », jamais « perdue ». --- -### 4. Rappel — clés ARB à prévoir côté app +## 2. ⚠️ La mise en garde sur `allocatedW` des bornes TOMBE -Deux motifs sont émis par le banc et n'ont pas encore de rendu français dans `telemetry_text.dart` : +Vous portiez : *« pour une borne, `allocatedW` est la décision de l'arbitre et non l'état de la +borne »*. C'était juste, et ça ne l'est plus. -- **`BELOW_MIN_POWER`** — params `budgetW`, `minPowerW`, et `state` pour les mécanismes à états - (SG-Ready). Déjà traité dans `telemetry_text.dart`, à vérifier côté ARB. -- **`BATTERY_RESERVE`** — params **`socPercent`, `reservePercent`, `withheldW`**. Le - `kCodesReserveBatterie` de `telemetry_text.dart` est encore **vide**, et les clés qu'il - cherche (`batteryLevel`/`soc`, `threshold`/`batteryLevelConsideration`) **ne sont pas celles - publiées**. À aligner sur les trois noms ci-dessus. +**Le défaut derrière cette mise en garde** : jusqu'à `+etm22`, l'arbitre décidait et publiait, +puis `adjustEvChargers()` — le chemin d'exécution hérité de l'amont — re-décidait derrière lui +et commandait autre chose. La décision publiée n'atteignait pas le matériel. -Et un réglage à exposer dans l'écran installateur : **`batteryLevelConsideration`** — son défaut -d'usine est passé de 0,9 à **0,2** (à 0,9 avec une batterie à 50 %, le HEMS ne pilote plus rien, -sans erreur ni panne). En revanche **`minimalChargingCurrent` ne doit jamais être présenté comme -configurable** : il n'existe pas comme réglage, il se déduit de la borne et de la voiture. +**`+etm23` ferme ça.** Une charge, un commandeur : les actions EV du waterfall sont dispatchées +vers l'adaptateur, et les bornes ainsi commandées sortent du chemin proxy. Mesuré sur machine : +9 commandes émises à la borne sur la fenêtre d'observation, **9 par l'arbitre, 0 par le proxy**, +et 6 cycles sur 6 portent la ligne « commandée par le waterfall, pas de re-décision ici ». + +**Conséquence pour vous** : `allocationIsCommand` doit rendre `true` **partout**, et les +branches conditionnelles qui en dépendaient peuvent disparaître. + +**Une réserve, et elle est petite mais réelle** : l'allocation reste ce que l'arbitre a +**commandé**, pas ce que la borne **tire**. Un plafond matériel, un véhicule qui refuse ou un +câble débranché feront toujours diverger les deux. Ce que vous pouvez enfin affirmer, c'est que +*personne d'autre ne commande*. Pour l'écart commande/réalité, la charge utile `mechanism` de la +borne porte `chargingEnabled`, `currentA`, `phaseCount` et `pluggedIn` — c'est là qu'il se lit, +pas dans `allocatedW`. + +--- + +## 3. Nouvelle clé ARB : `EV_GRID_START` + +``` +EV_GRID_START { budgetW, floorW, gridW } funding: "grid" +``` + +**Une borne qui DÉMARRE en soutirant au réseau.** Le surplus ne paie pas son plancher, mais le +réglage `acquisitionTolerance` (défaut 0,5) autorise l'appoint : à 0,5, une borne qui exige +4 140 W démarre dès 2 070 W de surplus et tire le reste du réseau. + +`gridW = floorW − budgetW` est **ce qui est acheté**. C'est le chiffre qui fait de cette ligne +une décision, et pas un effet de bord : il mérite d'être montré, pas rangé dans un repli. + +**Comptabilité — c'est le seul cas où l'`allocatedW` d'une charge se partage** entre les deux +compteurs du budget : `budgetW` vient du surplus et entre dans `budget.allocatedW`, `gridW` +vient du réseau et entre dans `budget.evReservedW`. `budgetW + gridW == allocatedW`, toujours. +Si vous réconciliez la somme des allocations avec le budget, c'est la ligne à traiter à part. + +**Borné par construction** : le démarrage exige du surplus réel et consomme tout le budget +restant, donc **au plus une borne par cycle** peut soutirer. + +> **Pas encore vu sur machine.** Guetté six cycles au banc, jamais fabriqué : le budget de la +> borne est passé de 385 W à 2 350 W sans s'arrêter dans la fenêtre de tolérance. Le motif est +> éprouvé par la simulation. Prévoyez la clé, mais ne construisez pas d'écran autour d'une +> chose que le terrain n'a pas encore montrée. + +Seconde clé du même lot, plus rare : `PHASE_LIMIT { limitW, requiredW }` — c'est la **limite de +phase** qui interdit, pas le budget. Deux causes, deux gestes : l'une se règle en délestant la +maison ou en revoyant l'abonnement, l'autre en attendant le soleil. + +--- + +## 4. Le rang d'une borne est configurable, et il se classe avec les autres + +`LoadConfig.priority`, comme toute autre charge. **Il n'y a pas de second système de priorité** +et il n'y en aura pas : un classement propre aux bornes ne saurait pas exprimer +« VE1 > ECS > VE2 », et deux classements qui ne peuvent pas s'interclasser sont un défaut, pas +deux fonctionnalités. `ChargingInfo` ne porte donc **aucun** champ de rang. + +**Le glisser-déposer marche donc sur une seule liste**, bornes comprises, et se pose par +`SetLoadConfig` comme le reste. Vérifié sur machine, y compris l'inversion de rang entre deux +charges. + +Le tri est **total** : à rang égal, l'identifiant départage. Laquelle est servie est donc +reproductible d'un cycle à l'autre, même avant tout réglage. + +> **Réserve sur le rang par défaut, et elle vous concerne.** À la création automatique, la borne +> reçoit « le plus petit rang existant moins un, borné à 1 ». Quand une charge occupe déjà le +> rang 1 — c'est le cas du banc — l'intention « en tête » **dégénère en égalité** : les deux +> bornes et le chauffe-eau sont tous à 1. Sans conséquence sur la reproductibilité, mais +> **l'ordre affiché à l'installateur avant qu'il n'ait rien réglé n'est pas un choix, c'est un +> artefact**. Ne le présentez pas comme un réglage tant que le point n'est pas tranché côté +> moteur (`docs/RELEVE_3g2.md` §3.2). Les vrais rangs se posent à la mise en service, par vous. + +--- + +## Ce qui n'a PAS changé, et qu'il ne faut pas déduire + +- **L'échéance de départ et le tarif dynamique restent au proxy**, financés au réseau + (`funding: "grid"`, motifs `EV_DEADLINE` / `EV_SPOT_MARKET`). Leur puissance vit dans + `budget.evReservedW`, pas dans `budget.allocatedW`. +- **`chargingState` de `ChargingInfo`** garde son sens et reste publié par l'arbitre pour les + bornes qu'il commande. +- **`SetChargingInfo` n'est toujours pas partiel** — l'objet remplace le stocké, et un champ + writable omis retombe sur son défaut. La description publiée le dit désormais, et + `INTERFACE.md` en donne la table. Lire → modifier → renvoyer complet. +- **`repeatDays` va de 1 (lundi) à 7 (dimanche)**, pas de 0 à 6. La documentation disait le + contraire ; le code a toujours refusé le 0. diff --git a/lib/widgets/ev_charging_card.dart b/lib/widgets/ev_charging_card.dart index 5f324e3..9230256 100644 --- a/lib/widgets/ev_charging_card.dart +++ b/lib/widgets/ev_charging_card.dart @@ -299,7 +299,7 @@ class _EVChargingCardState extends State { // Hors échéance, l'app n'a pas de cible : elle n'en montre pas. if (_deadlineEnabled) ...[ const SizedBox(height: 14), - _SocProgress(targetSoc: _targetSoc, currentSoc: 62), + _SocProgress(targetSoc: _targetSoc), ], ], ), @@ -465,10 +465,29 @@ class _ModeButton extends StatelessWidget { } /// Barre de progression SOC de la voiture avec target et valeur courante. +/// Cible de charge du véhicule — **la cible seule, jamais un état de charge**. +/// +/// Cette carte affichait « 62 % » et « Aujourd'hui à 07:30 » en dur. Les deux sont +/// retirés, et il faut dire pourquoi le second correctif possible n'en est pas un : +/// +/// **aucun SOC de véhicule n'existe dans ce contrat.** Vérifié sur `.75` le 2026-08-27, +/// sur les 58 classes de la box : aucune classe portant l'interface `evcharger` ne +/// déclare d'état de charge, aucune classe 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 : la brancher ici afficherait la charge du bâtiment +/// en face d'une cible de voiture. Un chiffre faux et plausible est pire qu'un chiffre +/// absent — c'était déjà le défaut du 62 en dur, le remplacer par 50 le garderait en le +/// rendant crédible. +/// +/// Reste donc ce que l'app SAIT : la cible qu'elle vient elle-même d'écrire. La barre de +/// progression disparaît avec la mesure — une jauge sans grandeur mesurée est un dessin. class _SocProgress extends StatelessWidget { final int targetSoc; - final int currentSoc; - const _SocProgress({required this.targetSoc, required this.currentSoc}); + const _SocProgress({required this.targetSoc}); @override Widget build(BuildContext context) { @@ -484,24 +503,17 @@ class _SocProgress extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Charge prévue jusqu\'à $targetSoc%', + Text('Charge demandée jusqu\'à', style: EtmTokens.sans(size: 13, color: EtmTokens.navy)), - Text('$currentSoc%', style: EtmTokens.mono(size: 13, color: EtmTokens.blue)), + Text('$targetSoc%', + style: EtmTokens.mono(size: 13, color: EtmTokens.blue)), ], ), const SizedBox(height: 2), - Text('Aujourd\'hui à 07:30', style: EtmTokens.sans(size: 11, color: EtmTokens.mutedOf(context))), - const SizedBox(height: 8), - ClipRRect( - borderRadius: BorderRadius.circular(99), - child: SizedBox( - height: 7, - child: LinearProgressIndicator( - value: currentSoc / 100, - backgroundColor: const Color(0xFFD6E6F2), - valueColor: const AlwaysStoppedAnimation(EtmTokens.blue), - ), - ), + Text( + 'La box ne publie pas l\'état de charge du véhicule : l\'avancement ne peut ' + 'pas être affiché.', + style: EtmTokens.sans(size: 11, color: EtmTokens.mutedOf(context)), ), ], ),