TROIS DEMANDES DE PATRICK. 1. Le soutiré passe du rouge au BLEU. Avec l'orange partagé sur les deux barres, le rouge rendait l'image trop chaude — et le bleu est déjà la couleur de la maison partout ailleurs. Il ne reste d'orange que le bloc partagé, celui qui doit se voir. 2. Second graphe en vue barres : « Où est passée la consommation » — une barre empilée par période, une couleur par charge déclarée, plus le reste du foyer qui ferme la barre à la consommation totale. L'énergie vient du compteur cumulé de chaque appareil (`totalConsumption`), jamais d'une intégration de la puissance — intégrer supposerait la puissance constante entre deux échantillons. Vérifié à la sonde avant d'écrire : ECS 80,8 kWh et PAC 43,1 kWh sur la fenêtre, avec des deltas journaliers réels. Une charge dont le compteur n'a pas bougé donne 0 kWh, qui est une MESURE. Une charge dont le cumul manque à une borne n'a PAS de part. Les deux se ressemblent à l'écran et ne disent pas la même chose. Et un compteur qui recule est une remise à zéro, pas une consommation négative : la part est refusée plutôt que rendue absurde. 🔴 3. LE « 6/9 » EN DOUBLE N'ÉTAIT PAS UN DÉFAUT DE L'APP. C'est la trace de notre propre bascule de fuseau du 2026-09-06 : la frontière de journée de la box a glissé d'une heure, et l'historique porte deux échantillons pour le 06/09 — 00:00 et 01:00. Le graphe affichait fidèlement une « journée » d'une heure à côté de journées entières. La couture est PERMANENTE : elle est dans les données passées, pas dans une requête. Tout écran qui compare des périodes doit grouper par période civile, jamais par indices consécutifs. Aucune énergie n'est perdue — la tranche d'une heure est absorbée dans sa journée. Et une étiquette d'axe illisible : fl_chart en pose une à `min` en plus de celles de l'intervalle, et les deux se superposaient — « 20:4500 ». Deux heures imprimées l'une sur l'autre ne se lisent ni l'une ni l'autre. 358 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
126 lines
5.2 KiB
Dart
126 lines
5.2 KiB
Dart
/// **Qui consomme, dans le temps** — `Energy.GetThingPowerLogs`, une série par appareil.
|
|
///
|
|
/// ## Ce que cet objet porte en plus des courbes
|
|
///
|
|
/// La liste de ce qu'on a **demandé sans l'obtenir**. Six Things sondés sur `.75` le
|
|
/// 2026-09-08, cinq répondent : la V2C Trydan est absente parce que sa classe ne déclare
|
|
/// pas l'interface `smartmeter`. Une réponse partielle sans la liste des manquants se lit
|
|
/// comme une réponse complète, et l'écran tracerait alors quatre courbes en laissant croire
|
|
/// qu'il n'y en avait que quatre à tracer.
|
|
///
|
|
/// **Une série absente ne se dessine pas à zéro.** Une ligne plate affirme « cette charge
|
|
/// n'a rien tiré » ; l'absence dit « on ne sait pas ». La borne V2C tire peut-être 4 kW en
|
|
/// ce moment — sa puissance instantanée est lisible — elle n'a simplement pas de passé.
|
|
///
|
|
/// ## Le signe des sous-compteurs n'est PAS vérifié
|
|
///
|
|
/// 🔴 Sur le banc, `PAC-Meter`, `ECS-Meter` et la borne simulée publient **0 W à tous les
|
|
/// pas**, toute la journée. Aucun échantillon non nul n'existe, donc **rien ne dit si un
|
|
/// consommateur publie sa puissance en positif ou en négatif** — le compteur PV, lui, la
|
|
/// publie en négatif, ce qui interdit de supposer.
|
|
///
|
|
/// La valeur est donc transportée **brute**, et [ConsumerHistory.signeIndetermine] dit
|
|
/// quand la question reste ouverte. Poser un `.abs()` ici trancherait par défaut, et un
|
|
/// `.abs()` qui devine juste par hasard est indiscernable d'un `.abs()` qui devine faux.
|
|
library;
|
|
|
|
/// Un point d'une série : un instant, la puissance publiée, et le compteur cumulé.
|
|
class ConsumerPoint {
|
|
final DateTime t;
|
|
final double powerW;
|
|
|
|
/// `totalConsumption` de l'appareil, en kWh cumulés. `null` = non publié.
|
|
///
|
|
/// C'est LUI qui donne l'énergie d'une période, par différence entre deux points — jamais
|
|
/// une intégration de [powerW], qui supposerait que la puissance est restée constante
|
|
/// entre deux échantillons.
|
|
final double? cumulKwh;
|
|
|
|
const ConsumerPoint(this.t, this.powerW, {this.cumulKwh});
|
|
}
|
|
|
|
/// La série d'un appareil, avec de quoi le nommer et le colorer.
|
|
class ConsumerSeries {
|
|
final String thingId;
|
|
final String label;
|
|
|
|
/// `ecs` · `heating` · `ev`, ou `null` si la charge n'a pas de domaine déclaré.
|
|
/// `null` n'appelle pas de couleur par défaut : voir `EtmTokens.domaineOf`.
|
|
final String? domaine;
|
|
|
|
final List<ConsumerPoint> points;
|
|
|
|
const ConsumerSeries({
|
|
required this.thingId,
|
|
required this.label,
|
|
required this.domaine,
|
|
required this.points,
|
|
});
|
|
|
|
/// Vrai si la série ne contient que des zéros — **une mesure, pas un trou**.
|
|
///
|
|
/// Un compteur dédié qui lit 0 W dit « rien ne chauffe ». C'est exactement ce qu'il faut
|
|
/// tracer : une ligne au ras de l'axe, pas une absence.
|
|
bool get toutAZero => points.isNotEmpty && points.every((p) => p.powerW == 0);
|
|
|
|
/// L'énergie consommée entre deux instants, ou `null` si l'un des deux cumuls manque.
|
|
///
|
|
/// `null` n'est pas zéro : une période dont le compteur n'a pas été publié n'a pas de
|
|
/// barre, elle n'a pas une barre vide.
|
|
double? energieEntre(DateTime a, DateTime b) {
|
|
double? cumul(DateTime t) {
|
|
for (final p in points) {
|
|
if (p.t == t) return p.cumulKwh;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
final x = cumul(a), y = cumul(b);
|
|
if (x == null || y == null) return null;
|
|
// Un compteur ne recule pas : un delta négatif est une remise à zéro, pas une
|
|
// consommation négative. On refuse plutôt que de rendre un nombre absurde.
|
|
final d = y - x;
|
|
return d >= 0 ? d : null;
|
|
}
|
|
|
|
double get maxW =>
|
|
points.isEmpty ? 0 : points.map((p) => p.powerW.abs()).reduce((a, b) => a > b ? a : b);
|
|
}
|
|
|
|
/// Le résultat complet d'une interrogation par appareil.
|
|
class ConsumerHistory {
|
|
/// Les séries obtenues, dans l'ordre où elles ont été demandées.
|
|
final List<ConsumerSeries> series;
|
|
|
|
/// **Les appareils demandés dont la box n'a rien renvoyé**, par leur nom d'affichage.
|
|
///
|
|
/// Ce n'est pas une erreur : un appareil sans interface de comptage n'a pas
|
|
/// d'historique, et c'est un fait sur l'appareil, pas sur la requête.
|
|
final List<String> sansHistorique;
|
|
|
|
const ConsumerHistory({required this.series, required this.sansHistorique});
|
|
|
|
static const ConsumerHistory vide =
|
|
ConsumerHistory(series: [], sansHistorique: []);
|
|
|
|
bool get estVide => series.isEmpty;
|
|
|
|
/// 🔴 **Vrai tant qu'aucun échantillon non nul n'a été vu.** Voir la note de tête : sans
|
|
/// une seule valeur non nulle, la convention de signe des sous-compteurs reste inconnue,
|
|
/// et l'écran ne doit pas présenter l'empilement comme une décomposition établie.
|
|
bool get signeIndetermine => series.every((s) => s.maxW == 0);
|
|
|
|
/// Les instants communs à toutes les séries — l'axe des abscisses de l'empilement.
|
|
///
|
|
/// Pris sur la série la plus longue : les compteurs n'échantillonnent pas tous au même
|
|
/// instant, et interpoler inventerait des points.
|
|
List<DateTime> get instants {
|
|
if (series.isEmpty) return const [];
|
|
var plusLongue = series.first;
|
|
for (final s in series) {
|
|
if (s.points.length > plusLongue.points.length) plusLongue = s;
|
|
}
|
|
return [for (final p in plusLongue.points) p.t];
|
|
}
|
|
}
|