Maquette validée par Patrick (couleurs comprises). Deux graphes, et trois défauts d'affichage trouvés en chemin — dont deux à l'écran seulement. GRAPHE 1 — les trois « on ne voit pas » avaient trois causes. production : publiée NÉGATIVE (−103 W). La magnitude est prise AU RENDU, pas au transport : le champ reste brut (§7.1), et `.abs()` est licite sur une production, dont le signe ne porte aucune information — contrairement au réseau, où le signe dit le sens (d'où gridImportW/gridExportW là-bas). autoconsommation : `production − export` clampé à 0, donc TOUJOURS zéro avec une production négative. Même racine, corrigée par la même magnitude. SOC : correctement absent — aucune batterie sur l'installation. Ce qui manquait, c'était de le DIRE ; c'est maintenant une note sous le graphe. ⚠️ Et un deuxième empêchement, vu seulement à l'écran : le signe réparé, la production restait invisible SOUS l'autoconsommation. Tant que rien n'est exporté les deux sont égales au pixel près, et la verte était tracée par dessus. L'autoconso passe en pointillé — des tirets sur la bande ambre disent « tout est consommé sur place », leur décrochage dit l'export. Deux courbes qui coïncident doivent pouvoir se VOIR coïncider. ⚠️ Ce correctif en créait un autre : le SOC était aussi vert pointillé. Sur une installation AVEC batterie les deux auraient été indiscernables — collision qu'aucun banc sans batterie ne pouvait montrer. Le SOC passe en indigo. GRAPHE 2 — qui consomme, empilé. `Energy.GetThingPowerLogs`, et la liste des appareils vient de GetLoadConfig : chaque charge déclarée porte son `meterThingId`, son label et son domaine — vérifié renseigné sur les quatre charges de .75. Rien n'est deviné d'un nom d'appareil. Un appareil demandé sans réponse sort dans `sansHistorique` et est NOMMÉ sous le graphe, jamais tracé à zéro : la V2C ne déclare pas `smartmeter`, donc elle n'a pas de passé — ce qui n'est pas « elle n'a rien tiré ». Le RESTE du foyer est licite ici, contrairement au Sankey qui l'avait fait interdire : chaque part est un compteur dédié et le total est le compteur du foyer, donc la soustraction ne porte que du mesuré. Et un reste NÉGATIF n'est pas raboté — c'est la preuve qu'un sous-compteur n'est pas dans le périmètre du total, et l'écraser effacerait la seule chose qui le dise. 🔴 Le signe des sous-compteurs n'est PAS vérifié : les trois lisent 0 W à tous les pas, donc aucun échantillon non nul n'existe. Le compteur PV publie en négatif — rien n'autorise à supposer. La valeur est transportée brute et `signeIndetermine` le dit à l'écran. Couleurs par consommateur ajoutées au thème (pac ambre, ecs sarcelle, ev indigo) : distinctes des couleurs de FLUX, qui disent d'où vient l'énergie, là où celles-ci disent qui la prend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
98 lines
4.1 KiB
Dart
98 lines
4.1 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 et la puissance publiée à cet instant.
|
|
class ConsumerPoint {
|
|
final DateTime t;
|
|
final double powerW;
|
|
const ConsumerPoint(this.t, this.powerW);
|
|
}
|
|
|
|
/// 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);
|
|
|
|
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];
|
|
}
|
|
}
|