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
179 lines
7.9 KiB
Dart
179 lines
7.9 KiB
Dart
/// Le **bilan du jour**, sur l'appareil, contre les vrais compteurs de `.75`.
|
|
///
|
|
/// ## Pourquoi il faut le voir sur appareil
|
|
///
|
|
/// `test/daily_balance_test.dart` prouve le calcul sur une fixture. Il ne prouve ni que
|
|
/// les bornes de minuit local sont les bonnes une fois dans le fuseau du téléphone, ni que
|
|
/// le décodage des clés de log tient bout à bout, ni que les tuiles se remplissent après
|
|
/// le premier rendu — le bilan arrive **après** la première image, il faut un aller-retour.
|
|
///
|
|
/// ## LECTURE SEULE — le banc est en préparation
|
|
///
|
|
/// La V2C arrive, `.75` doit rester dans l'état où la prépa l'a laissé. Ce test n'écrit
|
|
/// **rien** : ni `SetLoadConfig`, ni `SetChargingInfo`, ni réglage. Il lit des logs et
|
|
/// regarde un écran.
|
|
///
|
|
/// Lancement :
|
|
/// ```sh
|
|
/// flutter test integration_test/daily_balance_on_device_test.dart -d dee659e2
|
|
/// ```
|
|
library;
|
|
|
|
import 'package:etm_powersync_app/main.dart' as app;
|
|
import 'package:etm_powersync_app/models/daily_balance.dart';
|
|
import 'package:etm_powersync_app/services/connection_manager.dart';
|
|
import 'package:etm_powersync_app/screens/dashboard_screen.dart';
|
|
import 'package:etm_powersync_app/services/nymea_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:integration_test/integration_test.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
const _hote = '192.168.1.75';
|
|
|
|
Future<void> _attendre(
|
|
WidgetTester tester,
|
|
bool Function() cond, {
|
|
Duration limite = const Duration(seconds: 30),
|
|
String quoi = 'condition',
|
|
}) async {
|
|
final fin = DateTime.now().add(limite);
|
|
while (DateTime.now().isBefore(fin)) {
|
|
await tester.pump(const Duration(milliseconds: 250));
|
|
if (cond()) return;
|
|
}
|
|
fail('délai dépassé en attendant : $quoi');
|
|
}
|
|
|
|
void main() {
|
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
testWidgets('le bilan du jour, sur appareil, contre .75', (tester) async {
|
|
final debordements = <String>[];
|
|
final precedent = FlutterError.onError;
|
|
FlutterError.onError = (details) {
|
|
final t = details.exceptionAsString();
|
|
if (t.contains('overflowed by')) {
|
|
final l = t.split('\n').first;
|
|
if (!debordements.contains(l)) debordements.add(l);
|
|
return;
|
|
}
|
|
precedent?.call(details);
|
|
};
|
|
addTearDown(() => FlutterError.onError = precedent);
|
|
|
|
app.main();
|
|
await tester.pump(const Duration(seconds: 3));
|
|
|
|
final ctx = tester.element(find.byType(MaterialApp).first);
|
|
final svc = Provider.of<NymeaService>(ctx, listen: false);
|
|
final cnx = Provider.of<ConnectionManager>(ctx, listen: false);
|
|
if (!svc.isConnected) {
|
|
// ignore: unawaited_futures
|
|
cnx.connectNew(host: _hote);
|
|
}
|
|
await _attendre(tester, () => svc.isConnected,
|
|
limite: const Duration(seconds: 60), quoi: 'la connexion à $_hote');
|
|
debugPrint('[appareil] connecté à $_hote');
|
|
|
|
// ── 1. Le bilan tel que le service le lit ────────────────────────────────
|
|
final DailyBalance? b = await svc.fetchDailyBalance();
|
|
expect(b, isNotNull,
|
|
reason: '.75 publie des logs depuis minuit — la journée doit être mesurable');
|
|
|
|
debugPrint('[appareil] fenêtre ${b!.from} → ${b.to} · ${b.sampleCount} échantillons');
|
|
debugPrint('[appareil] prod ${b.productionKwh.toStringAsFixed(2)} · '
|
|
'conso ${b.consumptionKwh.toStringAsFixed(2)} · '
|
|
'import ${b.importKwh.toStringAsFixed(2)} · '
|
|
'export ${b.exportKwh.toStringAsFixed(2)} kWh');
|
|
debugPrint('[appareil] autoconso ${b.selfConsumptionRate?.toStringAsFixed(1)} % · '
|
|
'autonomie ${b.autonomyRate?.toStringAsFixed(1)} % · '
|
|
'identité ${b.identityHolds ? "OK" : "FAUSSE"} · '
|
|
'partielle : ${b.startsLate}');
|
|
|
|
// ── 2. Les bornes sont bien celles du JOUR, dans le fuseau du téléphone ──
|
|
final maintenant = DateTime.now();
|
|
final minuit = DateTime(maintenant.year, maintenant.month, maintenant.day);
|
|
expect(b.from.isBefore(minuit), isFalse,
|
|
reason: 'la fenêtre ne doit pas déborder sur la veille — minuit LOCAL');
|
|
expect(b.to.isAfter(minuit), isTrue);
|
|
expect(b.sampleCount, greaterThanOrEqualTo(2));
|
|
|
|
// ── 3. L'ordre de grandeur tranche l'unité, bout à bout ─────────────────
|
|
//
|
|
// C'est le piège qui a fait afficher « 78 Wh » pour une journée de 78 kWh. Une maison
|
|
// ne produit ni des dixièmes de kWh ni des mégawattheures dans la journée.
|
|
expect(b.productionKwh, greaterThan(0.5),
|
|
reason: 'des Wh donneraient une valeur mille fois trop petite');
|
|
expect(b.productionKwh, lessThan(500));
|
|
|
|
// ── 4. L'identité des compteurs, sur la vraie machine ───────────────────
|
|
expect(b.identityHolds, isTrue,
|
|
reason: 'production + soutiré doit égaler consommation + injecté : '
|
|
'${b.productionKwh + b.importKwh} vs ${b.consumptionKwh + b.exportKwh}');
|
|
|
|
// ── 5. Aller AU tableau de bord ─────────────────────────────────────────
|
|
//
|
|
// Se connecter ne navigue pas : l'app reste sur l'écran Installations, et chercher les
|
|
// tuiles là-bas ne prouve que leur absence. Le contexte du `MaterialApp` est au-dessus
|
|
// du Router — on prend celui d'un Scaffold affiché.
|
|
GoRouter.of(tester.element(find.byType(Scaffold).first)).go('/');
|
|
await tester.pump(const Duration(seconds: 3));
|
|
await _attendre(
|
|
tester,
|
|
() => find.byType(DashboardScreen).evaluate().isNotEmpty,
|
|
quoi: 'le montage du tableau de bord',
|
|
);
|
|
debugPrint('[appareil] tableau de bord monté');
|
|
|
|
// ── 6. Les tuiles se remplissent APRÈS le premier rendu ─────────────────
|
|
//
|
|
// Le tableau de bord lit le bilan dans un post-frame callback : la première image
|
|
// porte « — ». Si l'anneau ne se réarmait pas, il resterait vide pour toujours.
|
|
await _attendre(
|
|
tester,
|
|
() => find.text('Autoconsommation').evaluate().isNotEmpty,
|
|
quoi: 'les tuiles du jour',
|
|
);
|
|
expect(find.text('Aujourd\'hui'), findsOneWidget,
|
|
reason: 'la période est dite UNE fois, en tête des quatre tuiles');
|
|
expect(find.text('Vers réseau'), findsOneWidget);
|
|
expect(find.text('Depuis réseau'), findsOneWidget);
|
|
expect(find.textContaining('cumulé').evaluate(), isEmpty,
|
|
reason: 'plus aucune tuile ne montre un compteur depuis l\'origine');
|
|
|
|
// Les libellés ne doivent pas être coupés au milieu d'un mot — « Autoconsomm… » est
|
|
// ce que la capture d'appareil a montré au premier passage, et aucun test unitaire ne
|
|
// pouvait le voir : ils ne rendent pas ces widgets à la largeur d'un vrai écran.
|
|
for (final t in tester.widgetList<Text>(find.byType(Text))) {
|
|
final d = t.data;
|
|
if (d != null && d.endsWith('…')) {
|
|
fail('libellé tronqué à l\'écran : « $d »');
|
|
}
|
|
}
|
|
|
|
// Laisser le temps au bilan d'arriver et à l'anneau de s'animer, puis regarder.
|
|
for (var i = 0; i < 60; i++) {
|
|
await tester.pump(const Duration(milliseconds: 250));
|
|
}
|
|
expect(find.text('— %').evaluate(), isEmpty,
|
|
reason: 'les anneaux doivent porter une valeur une fois le bilan lu');
|
|
|
|
debugPrint('[appareil] ÉCRAN « tableau de bord · bilan du jour » — maintenu 15 s');
|
|
for (var i = 0; i < 60; i++) {
|
|
await tester.pump(const Duration(milliseconds: 250));
|
|
}
|
|
|
|
if (debordements.isNotEmpty) {
|
|
debugPrint('[appareil] DÉBORDEMENTS (${debordements.length}) :');
|
|
for (final d in debordements) {
|
|
debugPrint('[appareil] • $d');
|
|
}
|
|
} else {
|
|
debugPrint('[appareil] aucun débordement de mise en page');
|
|
}
|
|
expect(debordements, isEmpty);
|
|
});
|
|
}
|