import 'package:flutter/material.dart'; import '../models/energy_data.dart' show ChargingMode; import '../models/ev_charger_live.dart'; import '../theme/etm_tokens.dart'; /// La tuile « Borne EV » des favoris — **rendue muette le 2026-08-28**. /// /// ## Ce qu'elle affichait, et pourquoi c'était grave /// /// Elle montrait « 3600 W », « 82 % solaire » et « Mode PV » : trois valeurs par défaut de /// `EnergyData` que l'app **n'a jamais lues sur la box**. Le mode n'était relu nulle part — /// il partait de `pv` et n'avançait qu'après une commande envoyée depuis l'app. Un client /// ouvrant ce favori voyait donc une borne en train de charger à 3,6 kW alors que rien /// n'était branché. /// /// La tuile **reste** — la retirer laisserait un trou dans une liste que le client a /// composée lui-même, sans explication. Elle dit maintenant ce que la box publie, et là où /// il n'y a rien à lire, elle le dit. /// /// ## Les trois grandeurs qui restent, et leur source /// /// | Affiché | Source | Absent = | /// |---|---|---| /// | branché / en charge | états `pluggedIn`, `charging` du Thing | « état non publié » | /// | puissance | état `currentPower` du Thing | « puissance non publiée » | /// | mode | `NymeaEnergy.GetChargingInfos` + `ChargingInfoChanged` | « mode non lu » | /// /// Pas de part solaire : la box ne dit pas d'où vient le courant d'une borne. Pas /// d'avancement de charge : LM-1009 §C1 — `chargeEnergy` est remis à zéro à chaque /// interruption, et l'égalité `sessionEnergy == chargeEnergy` relevée le 2026-08-28 ne /// prouve pas encore que le second y survive. /// /// ## Elle ne règle plus rien — et garde quand même l'avertissement /// /// Les trois boutons de mode sont partis le 2026-08-28 vers l'écran « Configurer » de la /// borne : deux endroits qui écrivent `SetChargingInfo` sont deux comportements qui /// divergent, et la mise en service se fait là-bas. Cette tuile est donc **en lecture /// seule**. /// /// **Mais la note « hors arbitrage » reste ici.** C'est le client qui ouvre ses favoris, /// pas l'installateur : si sa borne charge à pleine puissance hors du pilotage, il doit /// le voir là où il regarde — même s'il doit aller ailleurs pour le corriger. class EvFavoriteTile extends StatelessWidget { /// `null` = aucune borne déclarée sur l'installation. final EvChargerLive? live; const EvFavoriteTile({super.key, required this.live}); @override Widget build(BuildContext context) { // Copie locale : un champ nullable ne se promeut pas après un test, et la tuile lit // une douzaine de fois la même borne. final live = this.live; if (live == null) { return _Muet( icon: Icons.ev_station_outlined, titre: 'Borne de recharge', detail: 'Aucune borne déclarée sur cette installation.', ); } final couleur = live.charging == true ? EtmTokens.green : EtmTokens.blue; return Row( children: [ Container( width: 52, height: 52, decoration: BoxDecoration( color: couleur.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(14), ), child: Icon(Icons.ev_station_rounded, color: couleur, size: 28), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // La puissance MESURÉE, ou le fait qu'elle ne soit pas publiée. Jamais 0 W // par défaut : « 0 W » et « pas de mesure » sont deux choses différentes. if (live.currentPowerW != null) Text('${live.currentPowerW!.round()} W', style: EtmTokens.mono( size: 26, weight: FontWeight.w700, color: couleur)) else Text('Puissance non publiée', style: EtmTokens.sans( size: 13, color: EtmTokens.mutedOf(context))), Text(live.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: EtmTokens.sans(size: 12, color: couleur)), Text(_etat(live), style: EtmTokens.sans( size: 11, color: EtmTokens.mutedOf(context))), Text(_mode(live), style: EtmTokens.sans( size: 11, color: EtmTokens.mutedOf(context))), // Pleine puissance = hors arbitrage, et c'est le mode d'office d'une borne // détectée. Les trois boutons sont juste à côté : c'est l'endroit où le // dire coûte le moins à celui qui veut le corriger. if (live.isUnmanaged) ...[ const SizedBox(height: 3), Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.info_outline_rounded, size: 13, color: EtmTokens.amber), const SizedBox(width: 4), Expanded( child: Text(kEvUnmanagedNote, style: EtmTokens.sans( size: 10.5, color: EtmTokens.mutedOf(context))), ), ]), ], ], ), ), ], ); } /// Branché ? En charge ? — et « non publié » quand la borne ne le dit pas. String _etat(EvChargerLive live) { if (live.pluggedIn == null && live.charging == null) { return 'Branchement non publié par cette borne'; } if (live.charging == true) return 'En charge'; if (live.pluggedIn == true) return 'Véhicule branché, à l\'arrêt'; if (live.pluggedIn == false) return 'Aucun véhicule branché'; return 'À l\'arrêt'; } /// Le mode **que la box porte**, jamais celui que l'app a demandé. String _mode(EvChargerLive live) { if (live.apiMode == null) return 'Mode non lu'; final ui = live.uiMode; if (ui == null) return 'Mode non reconnu (${live.apiMode})'; final base = switch (ui) { ChargingMode.pv => 'PV', ChargingMode.minPv => 'Min+PV', ChargingMode.boost => 'Boost', }; return live.hasDeadlineMode ? 'Mode $base + échéance' : 'Mode $base'; } } /// Tuile sans donnée : elle nomme ce qu'elle attend, plutôt que d'afficher un zéro. class _Muet extends StatelessWidget { final IconData icon; final String titre; final String detail; const _Muet({required this.icon, required this.titre, required this.detail}); @override Widget build(BuildContext context) { return Row( children: [ Container( width: 52, height: 52, decoration: BoxDecoration( color: EtmTokens.mutedOf(context).withValues(alpha: 0.10), borderRadius: BorderRadius.circular(14), ), child: Icon(icon, color: EtmTokens.mutedOf(context), size: 26), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(titre, style: EtmTokens.sans(size: 14, weight: FontWeight.w600)), const SizedBox(height: 2), Text(detail, style: EtmTokens.sans( size: 11.5, color: EtmTokens.mutedOf(context))), ], ), ), ], ); } }