/// **Le graphe par consommateur — ce qu'il a le droit de dire, et ce qu'il doit taire.** /// /// Trois faits mesurés sur `.75` le 2026-09-08 tiennent tout ce fichier : /// /// - six Things demandés à `Energy.GetThingPowerLogs`, **cinq répondent** — la V2C Trydan /// ne déclare pas l'interface `smartmeter` ; /// - les trois sous-compteurs (PAC, ECS, borne simulée) lisent **0 W à tous les pas** ; /// - `production` est publiée **négative** par la box. /// /// Chacun crée une manière différente de mentir à l'écran, et aucune ne se voit sans y /// penser : une série absente tracée à zéro, un empilement présenté comme établi alors /// qu'aucune valeur non nulle ne l'a validé, et deux courbes invisibles pour une raison /// de signe. library; import 'package:etm_powersync_app/models/consumer_series.dart'; import 'package:etm_powersync_app/services/energy_ratios.dart'; import 'package:flutter_test/flutter_test.dart'; ConsumerSeries _serie(String id, String label, String? dom, List w) => ConsumerSeries( thingId: id, label: label, domaine: dom, points: [ for (int i = 0; i < w.length; i++) ConsumerPoint(DateTime(2026, 9, 8, 0, 15 * i), w[i]) ], ); void main() { group('une série ABSENTE n\'est pas une série à zéro', () { test('l\'appareil sans historique est NOMMÉ, pas escamoté', () { final h = ConsumerHistory( series: [_serie('ecs', 'chauffe-eau', 'ecs', [0, 0, 0])], sansHistorique: const ['V2C Trydan'], ); expect(h.series, hasLength(1)); expect(h.sansHistorique, ['V2C Trydan']); // Le contrôle qui MORD : la borne ne doit jamais devenir une série de zéros. expect(h.series.any((s) => s.label == 'V2C Trydan'), isFalse, reason: 'une ligne plate affirmerait « elle n\'a rien tiré », ' 'quand la vérité est « elle n\'a pas de passé »'); }); test('une série à zéro est une MESURE, et elle reste tracée', () { final s = _serie('pac', 'PAC banc', 'heating', [0, 0, 0, 0]); expect(s.toutAZero, isTrue); expect(s.points, hasLength(4), reason: 'un compteur dédié qui lit 0 W dit « rien ne chauffe » — ' 'c\'est exactement ce qu\'il faut montrer'); expect(s.maxW, 0); }); test('une série vide n\'est pas « toute à zéro »', () { // Deux états distincts sous une même apparence à l'écran : aucun point du tout // (rien demandé, rien reçu) et des points tous nuls (mesurés). const vide = ConsumerSeries( thingId: 'x', label: 'x', domaine: null, points: []); expect(vide.toutAZero, isFalse); }); }); group('le signe des sous-compteurs reste INVÉRIFIÉ tant que tout vaut zéro', () { test('trois compteurs à zéro : la question est ouverte, et le dit', () { final h = ConsumerHistory(series: [ _serie('pac', 'PAC banc', 'heating', [0, 0]), _serie('ecs', 'chauffe-eau', 'ecs', [0, 0]), ], sansHistorique: const []); expect(h.signeIndetermine, isTrue, reason: 'le compteur PV publie sa puissance en NÉGATIF sur cette box — ' 'rien n\'autorise à supposer la convention des consommateurs'); }); test('une seule valeur non nulle referme la question', () { final h = ConsumerHistory(series: [ _serie('pac', 'PAC banc', 'heating', [0, 0]), _serie('ecs', 'chauffe-eau', 'ecs', [0, 1500]), ], sansHistorique: const []); expect(h.signeIndetermine, isFalse); }); }); group('l\'axe des instants ne s\'invente pas', () { test('il vient de la série la plus longue, sans interpolation', () { final h = ConsumerHistory(series: [ _serie('a', 'a', null, [1, 2]), _serie('b', 'b', null, [1, 2, 3, 4]), ], sansHistorique: const []); expect(h.instants, hasLength(4)); }); test('sans série, aucun instant — et pas une liste de secours', () { expect(ConsumerHistory.vide.instants, isEmpty); expect(ConsumerHistory.vide.estVide, isTrue); }); }); group('la production NÉGATIVE, et les deux courbes qu\'elle effaçait', () { // Signalé par Patrick le 2026-09-08 : « dans le premier chart on ne voit pas la // production ni l'autoconsommation ». Une seule cause pour les deux. const productionPubliee = -103.0; // relevé sur .75 const acquisition = 3000.0; // soutirage, donc pas d'export test('telle quelle, l\'autoconsommation est clouée à zéro', () { final self = EnergyRatiosInterim.selfConsumptionPower( productionPubliee, acquisition); expect(self, 0, reason: '`production − export` puis clamp à 0 : avec une production ' 'négative le résultat ne peut JAMAIS être autre chose'); }); test('sur la magnitude, elle rend la vraie valeur', () { final self = EnergyRatiosInterim.selfConsumptionPower( productionPubliee.abs(), acquisition); expect(self, 103, reason: 'rien n\'est exporté, donc tout le produit est autoconsommé'); }); test('et l\'export reste soustrait quand il y en a', () { // Le `.abs()` porte sur la PRODUCTION seule. Le réseau garde son signe, parce que // là le signe dit le SENS — c'est ce qui distingue ce cas de celui du réseau. final self = EnergyRatiosInterim.selfConsumptionPower(5000, -1200); expect(self, 3800); }); }); }