feat(énergie): six cartes journalières, la V2C qui commence en cours de route, et plus de simulation muette
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
This commit is contained in:
parent
6d0ba26e07
commit
a76798f2d8
BIN
docs/captures/2026-09-08_energie-cartes-journalieres.png
Normal file
BIN
docs/captures/2026-09-08_energie-cartes-journalieres.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
BIN
docs/captures/2026-09-08_energie-empilement-v2c.png
Normal file
BIN
docs/captures/2026-09-08_energie-empilement-v2c.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
@ -4,6 +4,7 @@ import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/consumer_series.dart';
|
||||
import '../models/daily_balance.dart';
|
||||
import '../models/energy_data.dart';
|
||||
import '../models/nymea_models.dart';
|
||||
import '../providers/load_config_provider.dart';
|
||||
@ -68,6 +69,10 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
List<HistoryEntry> _socData = [];
|
||||
ConsumerHistory _conso = ConsumerHistory.vide;
|
||||
|
||||
/// Le bilan du JOUR — indépendant de l'onglet de période choisi juste en dessous.
|
||||
/// C'est délibéré : les tuiles disent la journée, les graphes disent la fenêtre.
|
||||
DailyBalance? _bilan;
|
||||
|
||||
/// Vrai quand la box a livré ses Things ET qu'aucun ne porte de batterie.
|
||||
///
|
||||
/// Les deux moitiés comptent : tant que les Things ne sont pas chargés, « pas de
|
||||
@ -91,8 +96,19 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_nymeaService = context.read<NymeaService>();
|
||||
if (!_nymeaService!.connected) _nymeaService!.startSimulation();
|
||||
_nymeaService!.addListener(_onServiceChangedForSoc);
|
||||
// 🔴 **Plus d'auto-simulation ici.** L'écran faisait
|
||||
// `if (!connected) startSimulation()` : monté avant que la WebSocket soit établie —
|
||||
// le cas NORMAL, puisqu'on y arrive en tapant l'installation — il basculait tout le
|
||||
// service en démo, et n'en ressortait jamais.
|
||||
//
|
||||
// Vu à l'écran le 2026-09-08, box parfaitement joignable : une sinusoïde orange et
|
||||
// une courbe de SOC à 75 % sur une installation **qui n'a pas de batterie**. Rien ne
|
||||
// distinguait ces données inventées des vraies — ni couleur, ni mention, ni forme.
|
||||
// C'est 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.
|
||||
_nymeaService!.addListener(_onServiceChanged);
|
||||
_fetch();
|
||||
});
|
||||
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (_) {
|
||||
@ -103,13 +119,25 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshTimer?.cancel();
|
||||
_nymeaService?.removeListener(_onServiceChangedForSoc);
|
||||
_nymeaService?.removeListener(_onServiceChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onServiceChangedForSoc() {
|
||||
if (!mounted || _initialSocFetched || _loading) return;
|
||||
if (_nymeaService?.batterySOCSource != null && _socData.isEmpty) {
|
||||
/// Rattrape ce qui n'était pas encore disponible au montage.
|
||||
///
|
||||
/// Deux cas, et le premier est la raison d'être de ce listener depuis qu'on ne simule
|
||||
/// plus : la **connexion** peut arriver après l'écran. Sans ce rattrapage, l'écran
|
||||
/// resterait vide jusqu'au tick de cinq minutes.
|
||||
void _onServiceChanged() {
|
||||
if (!mounted || _loading) return;
|
||||
final svc = _nymeaService;
|
||||
if (svc == null) return;
|
||||
|
||||
if (svc.connected && _data.isEmpty) {
|
||||
_fetch();
|
||||
return;
|
||||
}
|
||||
if (!_initialSocFetched && svc.batterySOCSource != null && _socData.isEmpty) {
|
||||
_initialSocFetched = true;
|
||||
_fetchSocOnly();
|
||||
}
|
||||
@ -168,12 +196,14 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
: Future<List<HistoryEntry>>.value([]),
|
||||
service.fetchThingPowerLogs(
|
||||
appareils: appareils, from: from, to: to, sampleRate: tab.sampleRate),
|
||||
service.fetchDailyBalance(),
|
||||
]);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_data = results[0] as List<PowerBalanceEntry>;
|
||||
_socData = results[1] as List<HistoryEntry>;
|
||||
_conso = results[2] as ConsumerHistory;
|
||||
_bilan = results[3] as DailyBalance?;
|
||||
_loading = false;
|
||||
_noData = _data.isEmpty;
|
||||
});
|
||||
@ -247,7 +277,7 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// KPI 2x2
|
||||
_KpiSection(service: service),
|
||||
_KpiSection(bilan: _bilan),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Sélecteur période
|
||||
@ -588,19 +618,38 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
final series = _conso.series;
|
||||
final n = _data.length;
|
||||
|
||||
// Alignement par INDEX, et seulement si les longueurs coïncident : les compteurs
|
||||
// n'échantillonnent pas tous au même instant, et interpoler inventerait des points.
|
||||
double valeurAt(ConsumerSeries s, int i) =>
|
||||
s.points.length == n ? s.points[i].powerW : 0.0;
|
||||
final alignees = series.where((s) => s.points.length == n).toList();
|
||||
// ⚠️ **Alignement par INSTANT, pas par index** — et une série peut légitimement
|
||||
// commencer au milieu du graphe.
|
||||
//
|
||||
// La V2C Trydan a reçu son comptage le 2026-09-08 à 10:57 (`powersync-plugin-v2c
|
||||
// +etm14`) : sur une fenêtre de 24 h elle rend **1 point contre 45** aux autres. Le
|
||||
// filtre précédent (« même longueur, sinon on ignore ») l'aurait écartée du tracé
|
||||
// **en la laissant dans la légende** — une légende qui promet une bande qu'on ne
|
||||
// dessine pas, le défaut même qu'on venait de corriger sur l'autre graphe.
|
||||
//
|
||||
// Une série ne contribue donc que **là où elle a des points**. Avant son premier
|
||||
// point, elle n'apporte rien à la pile — et l'énergie qu'elle a consommée est
|
||||
// comptée dans le reste du foyer, ce qui est **arithmétiquement juste** : le total
|
||||
// reste le compteur du foyer, seule la RÉPARTITION est incomplète. C'est ce que dit
|
||||
// la note, et c'est une phrase différente de « elle a consommé zéro ».
|
||||
final index = {
|
||||
for (final s in series)
|
||||
s.thingId: {
|
||||
for (final p in s.points) p.t.millisecondsSinceEpoch: p.powerW
|
||||
}
|
||||
};
|
||||
double? valeurAt(ConsumerSeries s, int i) =>
|
||||
index[s.thingId]?[_data[i].timestamp.millisecondsSinceEpoch];
|
||||
|
||||
// Cumuls empilés, base = reste du foyer.
|
||||
final restes = <double>[];
|
||||
for (int i = 0; i < n; i++) {
|
||||
final somme = alignees.fold<double>(0, (a, s) => a + valeurAt(s, i));
|
||||
final somme =
|
||||
series.fold<double>(0, (a, s) => a + (valeurAt(s, i) ?? 0));
|
||||
restes.add(_data[i].consumptionW.abs() - somme);
|
||||
}
|
||||
final resteNegatif = restes.any((v) => v < -1);
|
||||
final alignees = series;
|
||||
|
||||
final bandes = <({String label, Color couleur, List<double> valeurs})>[
|
||||
(
|
||||
@ -613,7 +662,7 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
label: s.label,
|
||||
couleur: EtmTokens.domaineOf(context, s.domaine) ??
|
||||
EtmTokens.mutedOf(context),
|
||||
valeurs: [for (int i = 0; i < n; i++) valeurAt(s, i)]
|
||||
valeurs: [for (int i = 0; i < n; i++) valeurAt(s, i) ?? 0.0]
|
||||
),
|
||||
];
|
||||
|
||||
@ -706,7 +755,25 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
/// n'entre pas dans `GetThingPowerLogs` — alors que sa puissance instantanée est lisible.
|
||||
Widget _notesConsommateurs() {
|
||||
final manquants = _conso.sansHistorique;
|
||||
if (manquants.isEmpty && !_conso.signeIndetermine) {
|
||||
|
||||
// **Une série qui COMMENCE au milieu du graphe.**
|
||||
//
|
||||
// Cas rencontré le 2026-09-08 : la V2C a reçu son comptage à 10:57, donc sur 24 h
|
||||
// elle rend un point sur quarante-cinq. Le total de la pile reste juste — le foyer
|
||||
// est mesuré de bout en bout — mais la RÉPARTITION ne vaut qu'à partir de là. Le
|
||||
// taire ferait lire « cette charge n'a rien tiré avant », qui est le mensonge d'hier
|
||||
// sous une autre forme : ce n'est plus la présence de la série qui est en jeu, c'est
|
||||
// son début.
|
||||
final debut = _data.isEmpty ? null : _data.first.timestamp;
|
||||
final tardives = <(String, DateTime)>[
|
||||
for (final s in _conso.series)
|
||||
if (s.points.isNotEmpty &&
|
||||
debut != null &&
|
||||
s.points.first.t.isAfter(debut.add(const Duration(minutes: 2))))
|
||||
(s.label, s.points.first.t)
|
||||
];
|
||||
|
||||
if (manquants.isEmpty && tardives.isEmpty && !_conso.signeIndetermine) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
@ -720,6 +787,15 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
'publie pas de comptage. Sa puissance instantanée reste lisible.'
|
||||
: '${manquants.join(', ')} : pas d\'historique de puissance.',
|
||||
),
|
||||
for (final (label, t) in tardives)
|
||||
_NoteEcran(
|
||||
icon: Icons.start_rounded,
|
||||
texte: '$label n\'est mesurée que depuis '
|
||||
'${t.hour.toString().padLeft(2, '0')}:'
|
||||
'${t.minute.toString().padLeft(2, '0')} : avant, sa consommation est '
|
||||
'comptée dans le reste du foyer. Le total est juste, la répartition ne '
|
||||
'l\'est qu\'à partir de cette heure.',
|
||||
),
|
||||
if (_conso.signeIndetermine)
|
||||
_NoteEcran(
|
||||
icon: Icons.help_outline_rounded,
|
||||
@ -747,59 +823,132 @@ class _EnergyScreenState extends State<EnergyScreen> {
|
||||
|
||||
// ─────────────────────────── KPI section ───────────────────────────────────────
|
||||
|
||||
/// **Les six tuiles du haut — la JOURNÉE, et rien d'autre.**
|
||||
///
|
||||
/// Elles lisaient `service.energyData` : `dayProductionWh` y porte en réalité le compteur
|
||||
/// **cumulé depuis l'origine** de l'installation. Vu à l'écran le 2026-09-08 :
|
||||
/// « Production 62946.74… » au-dessus d'un graphe qui montre 24 heures. Et « Gains
|
||||
/// 8789.84 € » n'avait aucune source du tout — un prix du kWh que personne n'a saisi.
|
||||
///
|
||||
/// Elles lisent maintenant [DailyBalance], le même objet que le tableau de bord : une
|
||||
/// différence de compteurs sur la fenêtre `periodStartedAt → maintenant`, publiée par la
|
||||
/// box et non dérivée d'un fuseau.
|
||||
///
|
||||
/// ## Chaque taux porte son dénominateur
|
||||
///
|
||||
/// Décidé après le 2026-09-08 : « 70 % » se lisait comme une bonne matinée alors que
|
||||
/// l'installation avait produit **79 Wh** et acheté 6,4 kWh. Le nombre était exact ; la
|
||||
/// question à laquelle il répond n'intéressait personne.
|
||||
///
|
||||
/// Un seuil aurait demandé une constante inventée — et celle que j'avais proposée
|
||||
/// (0,05 kWh) aurait laissé passer ces 79 Wh. « 70 % sur 79 Wh produits » se lit juste
|
||||
/// toute l'année ; « 70 % » ne se lit juste que les bons jours. **Un ratio sans son
|
||||
/// dénominateur est une part sans son tout.**
|
||||
class _KpiSection extends StatelessWidget {
|
||||
final NymeaService service;
|
||||
const _KpiSection({required this.service});
|
||||
/// `null` = pas encore lu, ou journée non mesurable. Les deux se disent « — ».
|
||||
final DailyBalance? bilan;
|
||||
const _KpiSection({required this.bilan});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final d = service.energyData;
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_KpiTile(
|
||||
icon: Icons.wb_sunny_rounded,
|
||||
color: EtmTokens.amber,
|
||||
bgColor: EtmTokens.amberSoft,
|
||||
label: 'Production',
|
||||
value: _kwh(d.dayProductionWh),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
_KpiTile(
|
||||
icon: Icons.home_rounded,
|
||||
color: EtmTokens.blue,
|
||||
bgColor: EtmTokens.blueSoft,
|
||||
label: 'Consommation',
|
||||
value: _kwh(d.daySelfConsumptionWh),
|
||||
),
|
||||
],
|
||||
final b = bilan;
|
||||
String kwh(double? v) => v == null ? '—' : '${v.toStringAsFixed(2)} kWh';
|
||||
String pct(double? v) => v == null ? '—' : '${v.round()} %';
|
||||
|
||||
// « sur X produits » n'a de sens que s'il y a un X. Sans dénominateur, la tuile dit
|
||||
// pourquoi elle n'a pas de taux — pas un zéro, pas un tiret muet.
|
||||
final surProduction = b == null
|
||||
? null
|
||||
: b.productionKwh > 0
|
||||
? 'sur ${_court(b.productionKwh)} produits'
|
||||
: 'sans production';
|
||||
final surConsommation = b == null
|
||||
? null
|
||||
: b.consumptionKwh > 0
|
||||
? 'sur ${_court(b.consumptionKwh)} consommés'
|
||||
: 'sans consommation';
|
||||
|
||||
return Column(children: [
|
||||
Row(children: [
|
||||
_KpiTile(
|
||||
icon: Icons.wb_sunny_rounded,
|
||||
color: EtmTokens.amber,
|
||||
bgColor: EtmTokens.amberSoft,
|
||||
label: 'Production',
|
||||
value: kwh(b?.productionKwh),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
_KpiTile(
|
||||
icon: Icons.recycling_rounded,
|
||||
color: EtmTokens.green,
|
||||
bgColor: EtmTokens.greenSoft,
|
||||
label: 'Autoconsommation',
|
||||
value: '${d.selfConsumptionRate.toStringAsFixed(0)}%',
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
_KpiTile(
|
||||
icon: Icons.euro_rounded,
|
||||
color: EtmTokens.greenDark,
|
||||
bgColor: EtmTokens.greenSoft,
|
||||
label: 'Gains',
|
||||
value: '${d.dayGains.toStringAsFixed(2)} €',
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 14),
|
||||
_KpiTile(
|
||||
icon: Icons.home_rounded,
|
||||
color: EtmTokens.blue,
|
||||
bgColor: EtmTokens.blueSoft,
|
||||
label: 'Consommation',
|
||||
value: kwh(b?.consumptionKwh),
|
||||
),
|
||||
],
|
||||
);
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
Row(children: [
|
||||
_KpiTile(
|
||||
icon: Icons.north_west_rounded,
|
||||
color: EtmTokens.green,
|
||||
bgColor: EtmTokens.greenSoft,
|
||||
label: 'Vers réseau',
|
||||
value: kwh(b?.exportKwh),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
_KpiTile(
|
||||
icon: Icons.south_east_rounded,
|
||||
color: EtmTokens.importColor,
|
||||
bgColor: EtmTokens.amberSoft,
|
||||
label: 'Depuis réseau',
|
||||
value: kwh(b?.importKwh),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
Row(children: [
|
||||
_KpiTile(
|
||||
icon: Icons.recycling_rounded,
|
||||
color: EtmTokens.selfUse,
|
||||
bgColor: EtmTokens.amberSoft,
|
||||
label: 'Autoconsommation',
|
||||
value: pct(b?.selfConsumptionRate),
|
||||
detail: surProduction,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
_KpiTile(
|
||||
icon: Icons.bolt_rounded,
|
||||
color: EtmTokens.green,
|
||||
bgColor: EtmTokens.greenSoft,
|
||||
label: 'Autonomie',
|
||||
value: pct(b?.autonomyRate),
|
||||
detail: surConsommation,
|
||||
),
|
||||
]),
|
||||
// L'identité des compteurs est le seul contrôle disponible sur cette fenêtre.
|
||||
// Fausse, elle désigne un compteur cassé ou remis à zéro — et le bilan ne doit
|
||||
// alors pas être présenté comme une mesure.
|
||||
if (b != null && !b.identityHolds)
|
||||
const _NoteEcran(
|
||||
icon: Icons.rule_rounded,
|
||||
texte: 'Les compteurs ne se recoupent pas sur la journée : production + soutiré '
|
||||
'devrait égaler consommation + injecté. Un compteur a probablement été remis '
|
||||
'à zéro.',
|
||||
),
|
||||
if (b != null && b.startsLate)
|
||||
_NoteEcran(
|
||||
icon: Icons.schedule_rounded,
|
||||
texte: 'Journée partielle : la box ne publie rien avant '
|
||||
'${b.from.hour.toString().padLeft(2, '0')}:'
|
||||
'${b.from.minute.toString().padLeft(2, '0')}. Le bilan part de cette heure, '
|
||||
'pas de minuit.',
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
static String _kwh(double wh) => '${(wh / 1000).toStringAsFixed(2)} kWh';
|
||||
/// Une énergie courte : les watt-heures sous le kilowattheure, parce que « 0,08 kWh »
|
||||
/// se lit comme un arrondi et « 79 Wh » comme une mesure.
|
||||
static String _court(double kwh) =>
|
||||
kwh < 1 ? '${(kwh * 1000).round()} Wh' : '${kwh.toStringAsFixed(1)} kWh';
|
||||
}
|
||||
|
||||
class _KpiTile extends StatelessWidget {
|
||||
@ -809,10 +958,14 @@ class _KpiTile extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
/// Ce dont la valeur est la fraction — `null` quand elle n'est fraction de rien.
|
||||
final String? detail;
|
||||
|
||||
const _KpiTile({
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.bgColor,
|
||||
this.detail,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
@ -839,12 +992,33 @@ class _KpiTile extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: EtmTokens.sans(size: 11, color: EtmTokens.mutedOf(context))),
|
||||
// `scaleDown` dans une largeur BORNÉE (on est dans un `Expanded`) :
|
||||
// il réduit seulement quand il le faut. Sans lui, « Autoconsommation »
|
||||
// se coupait au milieu du mot — vu à l'écran le 2026-09-08.
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(label,
|
||||
maxLines: 1,
|
||||
style: EtmTokens.sans(
|
||||
size: 11, color: EtmTokens.mutedOf(context))),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(value,
|
||||
style: EtmTokens.mono(size: 15, weight: FontWeight.w700, color: color),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
if (detail != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(detail!,
|
||||
maxLines: 1,
|
||||
style: EtmTokens.sans(
|
||||
size: 10, color: EtmTokens.mutedOf(context))),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -991,8 +991,17 @@ class NymeaService extends ChangeNotifier {
|
||||
/// l'appareil (pas d'interface de comptage), pas une erreur de la requête, et le taire
|
||||
/// ferait lire une réponse partielle comme une réponse complète.
|
||||
///
|
||||
/// Vérifié sur `.75` le 2026-09-08 : six Things demandés, cinq répondent. La `V2C Trydan`
|
||||
/// est absente — sa classe déclare `evcharger · power`, pas `smartmeter`.
|
||||
/// ⚠️ **Le portier est `smartmeterconsumer`, pas `smartmeter`.** `smartmeter` est une
|
||||
/// interface VIDE, sans state requis, et ne conditionne rien. Le moteur teste
|
||||
/// `energymeter | smartmeterconsumer | smartmeterproducer | energystorage`
|
||||
/// (`EnergyManagerImpl::watchThing`) — c'est là que se décide qui a un historique.
|
||||
///
|
||||
/// Relevé le 2026-09-08 : six Things demandés, cinq répondaient, la `V2C Trydan` était
|
||||
/// absente. **Ce n'est plus vrai** — `powersync-plugin-v2c 1.15.2+etm14`, posé le même
|
||||
/// jour à 10:57, lui a ajouté `smartmeterconsumer` et elle répond depuis.
|
||||
///
|
||||
/// Le cas particulier a disparu, **pas le besoin** : une borne sans comptage (`kebaSimple`,
|
||||
/// `elvi`) ou une charge SG-Ready tombera toujours dans `sansHistorique`.
|
||||
///
|
||||
/// La charge utile porte `currentPower`, `totalConsumption`, `totalProduction` et
|
||||
/// `thingId`. **Seul `currentPower` est lu ici**, brut : voir `consumer_series.dart` pour
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user