fix(bilan): un cumul ABSENT n'est pas un cumul nul
`double d(String k) => (m[k] as num?)?.toDouble() ?? 0.0` : un champ manquant devenait 0,0 au décodage, deux bornes à zéro donnaient une différence à zéro, et l'écran affichait « 0,0 kWh · Vers réseau » comme une mesure. C'est le zéro forgé que nous traquons depuis une semaine, cette fois de NOTRE côté. Le cas est réel et n'attend pas sa cause : un compteur racine parfaitement en place, la puissance instantanée qui arrive, et les cumuls qui n'arrivent pas. `PowerBalanceEntry.totalsPresent` porte la distinction, et `fetchDailyBalance` refuse de présenter comme mesure un bilan bâti là-dessus — au même titre qu'un compteur corrompu. « Rien échangé aujourd'hui » et « la box ne publie pas ses cumuls » sont deux états, et le second n'a pas le droit de porter le premier. Le repli à 0 reste, pour ne pas propager des nullables dans tout l'écran Énergie, mais il est désormais ACCOMPAGNÉ du fait qu'il est un repli. Et `abs()` sur la production devient un choix DOCUMENTÉ plutôt qu'un correctif tacite : la production n'est pas signée, c'est le compteur qui l'est, donc la valeur absolue masquerait un compteur câblé à l'envers dont le signe serait la seule trace. Le correctif appartient à la source. On la garde en connaissance de cause — un signe faux se voit tous les jours, un câblage inversé ne se voit jamais — et on ne revient pas en arrière sans savoir ce que la box publiera. 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
a6dd319ef2
commit
a14adcfcc3
@ -350,7 +350,17 @@ class _NodeLayerState extends State<_NodeLayer>
|
||||
icon: Icons.wb_sunny_rounded,
|
||||
color: EtmTokens.solarOf(context),
|
||||
label: 'Solaire',
|
||||
// ⚠️ `currentPowerProduction` est publié NÉGATIF par nymea (-8 980 W =
|
||||
// ⚠️ **`abs()` est un choix, et il a un coût connu.** La production n'est
|
||||
// pas une grandeur signée : c'est le COMPTEUR qui l'est. Prendre la valeur
|
||||
// absolue redresse donc un signe de source côté affichage — et, ce faisant,
|
||||
// rendrait invisible un compteur câblé à l'envers, dont le signe serait la
|
||||
// seule trace. Le correctif appartient à la source ; il est demandé au
|
||||
// moteur. D'ici là on garde `abs()` en connaissance de cause : entre un
|
||||
// signe faux à l'écran et une anomalie de câblage masquée, le premier se
|
||||
// voit tous les jours, la seconde ne se voit jamais — et surtout, revenir
|
||||
// en arrière sans savoir ce que la box publiera ne serait pas un choix.
|
||||
//
|
||||
// `currentPowerProduction` est publié NÉGATIF par nymea (-8 980 W =
|
||||
// 8,98 kW produits), et le service garde les signes bruts par politique.
|
||||
// L'AFFICHAGE, lui, doit dire ce qu'un client comprend : une production est
|
||||
// une quantité positive. Vu sur appareil le 2026-09-05, « -8.9 kW Solaire »
|
||||
|
||||
@ -185,8 +185,21 @@ class PowerBalanceEntry {
|
||||
/// Soutiré au réseau, cumulé (kWh).
|
||||
final double totalAcquisitionKwh;
|
||||
|
||||
/// **Les quatre cumuls étaient-ils PRÉSENTS dans la charge utile ?**
|
||||
///
|
||||
/// ⚠️ Sans ce drapeau, un champ absent devient `0.0` au décodage — et un bilan bâti sur
|
||||
/// deux bornes à zéro s'affiche « 0,0 kWh · Vers réseau » comme s'il était mesuré. C'est
|
||||
/// le zéro forgé, du côté de l'écran cette fois : « rien échangé » et « la box ne le
|
||||
/// publie pas » sont deux états, et le second n'a pas le droit de porter le premier.
|
||||
///
|
||||
/// Le cas est réel : un compteur racine bien en place et des cumuls qui n'arrivent pas —
|
||||
/// une intégration incomplète côté box. La distinction doit tenir **quelle que soit la
|
||||
/// raison** de l'absence, donc sans attendre que cette raison soit corrigée.
|
||||
final bool totalsPresent;
|
||||
|
||||
const PowerBalanceEntry({
|
||||
required this.timestamp,
|
||||
this.totalsPresent = false,
|
||||
this.productionW = 0,
|
||||
this.consumptionW = 0,
|
||||
this.acquisitionW = 0,
|
||||
|
||||
@ -1001,7 +1001,19 @@ class NymeaService extends ChangeNotifier {
|
||||
return logs.map<PowerBalanceEntry>((e) {
|
||||
final m = e as Map<String, dynamic>;
|
||||
double d(String k) => (m[k] as num?)?.toDouble() ?? 0.0;
|
||||
// Un cumul ABSENT n'est pas un cumul nul. Le repli à 0 reste, pour ne pas
|
||||
// propager des nullables dans tout l'écran Énergie — mais il est désormais
|
||||
// ACCOMPAGNÉ du fait qu'il est un repli, et `fetchDailyBalance` refuse de
|
||||
// présenter comme mesure un bilan bâti là-dessus.
|
||||
const cumuls = [
|
||||
'totalProduction',
|
||||
'totalConsumption',
|
||||
'totalReturn',
|
||||
'totalAcquisition'
|
||||
];
|
||||
final presents = cumuls.every((k) => m[k] is num);
|
||||
return PowerBalanceEntry(
|
||||
totalsPresent: presents,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
(m['timestamp'] as num).toInt() * 1000),
|
||||
// Signes nymea BRUTS — ne corriger aucun signe (§7.1). L'autoconso
|
||||
@ -1100,6 +1112,22 @@ class NymeaService extends ChangeNotifier {
|
||||
final a = tries.first;
|
||||
final b = tries.last;
|
||||
|
||||
// ── CUMULS ABSENTS : on refuse, et pour la même raison qu'un compteur corrompu ──
|
||||
//
|
||||
// Un champ manquant est décodé à 0 (repli assumé, cf. `PowerBalanceEntry`). Deux
|
||||
// bornes à zéro donnent une différence à zéro, et l'écran afficherait « 0,0 kWh »
|
||||
// comme une mesure. « Rien échangé aujourd'hui » et « la box ne publie pas ses
|
||||
// cumuls » sont deux états, et le second n'a pas le droit de porter le premier.
|
||||
//
|
||||
// Le cas se produit avec un compteur racine parfaitement en place : la donnée
|
||||
// instantanée arrive, les cumuls non. La distinction tient donc ici, sans rien
|
||||
// attendre de la cause.
|
||||
if (!a.totalsPresent || !b.totalsPresent) {
|
||||
_log('[Bilan] cumuls absents de la charge utile — non mesurable, '
|
||||
'et surtout pas « 0 kWh »', force: true);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Compteurs corrompus : on refuse AVANT de soustraire. Deux valeurs absurdes peuvent
|
||||
// avoir une différence d'apparence saine.
|
||||
final bornes = [
|
||||
|
||||
@ -19,6 +19,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:etm_powersync_app/models/daily_balance.dart';
|
||||
import 'package:etm_powersync_app/models/energy_data.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Map<String, dynamic> _fixture() => jsonDecode(
|
||||
@ -46,6 +47,7 @@ DailyBalance _depuisFixture() {
|
||||
}
|
||||
|
||||
void main() {
|
||||
_cumulsAbsents();
|
||||
group('bilan du jour — sur le relevé réel de .75', () {
|
||||
test('les deltas sont ceux calculés hors de l\'app', () {
|
||||
final b = _depuisFixture();
|
||||
@ -149,3 +151,33 @@ void main() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// **Un cumul ABSENT n'est pas un cumul nul** — la distinction tient côté écran, quelle que
|
||||
/// soit la raison de l'absence.
|
||||
///
|
||||
/// Le cas est réel : un compteur racine parfaitement en place, la puissance instantanée qui
|
||||
/// arrive, et les cumuls qui n'arrivent pas. Décodés à 0, ils produisent un bilan de zéros
|
||||
/// que l'écran présenterait comme une mesure — « 0,0 kWh · Vers réseau » sur une journée
|
||||
/// dont on ne sait rien.
|
||||
void _cumulsAbsents() {
|
||||
group('cumuls absents : « non mesurable », jamais « 0 kWh »', () {
|
||||
test('le drapeau distingue le repli du vrai zéro', () {
|
||||
// Décodage d'une entrée SANS les quatre cumuls : les valeurs retombent à 0…
|
||||
final sansCumuls = PowerBalanceEntry(timestamp: DateTime(2026, 9, 6));
|
||||
expect(sansCumuls.totalsPresent, isFalse,
|
||||
reason: '…mais l\'écran doit pouvoir savoir que c\'était un repli');
|
||||
});
|
||||
|
||||
test('une entrée complète se déclare présente', () {
|
||||
final avec = PowerBalanceEntry(
|
||||
timestamp: DateTime(2026, 9, 6),
|
||||
totalsPresent: true,
|
||||
totalProductionKwh: 59.89,
|
||||
totalConsumptionKwh: 57.31,
|
||||
totalAcquisitionKwh: 26.31,
|
||||
totalReturnKwh: 28.89,
|
||||
);
|
||||
expect(avec.totalsPresent, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user